Compare commits

..

319 Commits

Author SHA1 Message Date
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
Archi
b16a459ab8 Misc 2023-12-05 01:53:14 +01:00
Archi
da2fd37aa1 Make STD work also in on-demand mode 2023-12-05 01:43:55 +01:00
Archi
8d1d508fe5 Use ASF's global database for STD package access tokens 2023-12-05 00:05:16 +01:00
Archi
92858de9e2 Misc 2023-12-05 00:04:38 +01:00
Archi
a7b1e01161 Revert "Disable server-side functionality in custom ASF builds"
This reverts commit 42ceb6d413.
2023-12-04 23:06:11 +01:00
renovate[bot]
e14d00b760 Update actions/setup-dotnet action to v4 (#3089)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-12-04 21:04:53 +01:00
ArchiBot
d737201ab5 Automatic translations update 2023-12-04 02:08:17 +00:00
Archi
5fcd5d51f9 Bump 2023-12-04 00:18:34 +01:00
Archi
fa74d98879 Kill API key leftovers 2023-12-03 21:51:32 +01:00
Archi
27f965d4af Kill API keys entirely
Economy bans we can handle ourselves server-side
2023-12-03 21:49:05 +01:00
Archi
ddd34d4a45 Bump 2023-12-03 17:01:55 +01:00
Archi
a3aa93fce8 Fix diff announcement with no items added/changed 2023-12-03 16:11:38 +01:00
Archi
0095d458e7 Handle new no requirements for API key 2023-12-03 14:08:24 +01:00
Archi
def3e26c92 Closes #3084 2023-12-03 13:48:51 +01:00
ArchiBot
10eb226722 Automatic translations update 2023-12-03 02:08:31 +00:00
renovate[bot]
3738ebed21 Update wiki digest to a9c7d7d 2023-12-02 20:06:57 +00:00
Archi
eff60bf307 Implement background announcements for ASF STM
This will be used exclusively by users with extraordinary large inventories, or if ASF backend will just be slower than usual.
2023-12-02 19:36:34 +01:00
Archi
b940af6e83 Misc 2023-12-02 18:49:32 +01:00
Archi
42ceb6d413 Disable server-side functionality in custom ASF builds 2023-12-02 18:37:40 +01:00
Archi
eef66cebf3 Add session data for SDA compatibility 2023-12-02 17:00:50 +01:00
Archi
fac8cb2c9a Misc 2023-12-02 15:16:26 +01:00
Archi
9ce195b4ec Use more correct way for resolving #3087 2023-12-02 15:14:19 +01:00
Archi
c4bcb679f9 Closes #3087 2023-12-02 15:08:59 +01:00
Archi
0d4871ca02 Fix delayed password input crash 2023-12-02 13:15:53 +01:00
renovate[bot]
8397a69130 Update ASF-ui digest to 62a5d46 2023-12-02 03:45:07 +00:00
ArchiBot
8515d72048 Automatic translations update 2023-12-02 02:06:10 +00:00
renovate[bot]
32af0abb6c Update ASF-ui digest to 7da00f0 2023-12-01 22:11:20 +00:00
renovate[bot]
e05f79b951 Update JetBrains/qodana-action action to v2023.2.9 2023-12-01 16:18:49 +00:00
renovate[bot]
9699686da5 Update ASF-ui digest to fe06a7c 2023-11-30 18:28:36 +00:00
ArchiBot
2052020d3d Automatic translations update 2023-11-30 02:08:00 +00:00
Archi
a894b7096a Bump 2023-11-29 20:36:24 +01:00
Archi
52b9d2d662 Revert market_fee_app
There are occurances where this is definitely wrong to use
2023-11-29 20:35:39 +01:00
Archi
f3cc0b938a Bump 2023-11-29 20:08:11 +01:00
Archi
80c362d5ed Fix regression in trades received now
Also improve that code while I'm at it
2023-11-29 19:52:43 +01:00
Archi
c7546194f8 Add handling of points shop items, and skip them for announcements 2023-11-29 19:17:02 +01:00
renovate[bot]
53993bfd34 Update ASF-ui digest to 3bea7c7 2023-11-29 13:36:45 +00:00
Archi
181bc28462 Bump 2023-11-29 14:36:25 +01:00
Archi
aea9dee4ea Further decrease server load
We can keep inventory checksum before deduplication in the cache. If it's determined to be the same, then our inventory state didn't change, so it also doesn't make much sense to ask server for set parts and announcement.
2023-11-29 14:26:57 +01:00
Archi
a3270e4081 Misc optimization 2023-11-29 13:21:12 +01:00
ArchiBot
ccf191f1ba Automatic translations update 2023-11-29 02:08:48 +00:00
Archi
a76b6fc32f Bump 2023-11-29 00:10:46 +01:00
270 changed files with 4733 additions and 2998 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)**

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

View File

@@ -27,7 +27,7 @@ jobs:
submodules: recursive
- name: Setup .NET Core
uses: actions/setup-dotnet@v3.2.0
uses: actions/setup-dotnet@v4.0.0
with:
dotnet-version: ${{ env.DOTNET_SDK_VERSION }}
@@ -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.8
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.4
with:
sarif_file: ${{ runner.temp }}/qodana/results/qodana.sarif.json

View File

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

@@ -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
@@ -80,7 +80,7 @@ jobs:
show-progress: false
- name: Setup .NET Core
uses: actions/setup-dotnet@v3.2.0
uses: actions/setup-dotnet@v4.0.0
with:
dotnet-version: ${{ env.DOTNET_SDK_VERSION }}
@@ -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.2
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.2
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.2
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.2
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.2
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.2
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.2
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.2
with:
name: windows-latest_ASF-win-arm64
path: out
- name: Download ASF-win-x64 artifact from windows-latest
uses: actions/download-artifact@v3.0.2
uses: actions/download-artifact@v4.1.2
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,19 +490,19 @@ 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:
artifacts: "out/*"
bodyFile: .github/RELEASE_TEMPLATE.md

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: 853672c0f7...cd1173a0d6

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

@@ -4,7 +4,7 @@
// / ___ \ | | | (__ | | | || | ___) || |_| __/| (_| || | | | | || _|| (_| || | | | | | | |
// /_/ \_\|_| \___||_| |_||_||____/ \__|\___| \__,_||_| |_| |_||_| \__,_||_| |_| |_| |_|
// |
// 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

@@ -4,7 +4,7 @@
// / ___ \ | | | (__ | | | || | ___) || |_| __/| (_| || | | | | || _|| (_| || | | | | | | |
// /_/ \_\|_| \___||_| |_||_||____/ \__|\___| \__,_||_| |_| |_||_| \__,_||_| |_| |_| |_|
// |
// 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

@@ -4,7 +4,7 @@
// / ___ \ | | | (__ | | | || | ___) || |_| __/| (_| || | | | | || _|| (_| || | | | | | | |
// /_/ \_\|_| \___||_| |_||_||____/ \__|\___| \__,_||_| |_| |_||_| \__,_||_| |_| |_| |_|
// |
// 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

@@ -4,7 +4,7 @@
// / ___ \ | | | (__ | | | || | ___) || |_| __/| (_| || | | | | || _|| (_| || | | | | | | |
// /_/ \_\|_| \___||_| |_||_||____/ \__|\___| \__,_||_| |_| |_||_| \__,_||_| |_| |_| |_|
// |
// 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 +21,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 +46,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 +140,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

@@ -4,7 +4,7 @@
// / ___ \ | | | (__ | | | || | ___) || |_| __/| (_| || | | | | || _|| (_| || | | | | | | |
// /_/ \_\|_| \___||_| |_||_||____/ \__|\___| \__,_||_| |_| |_||_| \__,_||_| |_| |_| |_|
// |
// 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 +21,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

@@ -4,7 +4,7 @@
// / ___ \ | | | (__ | | | || | ___) || |_| __/| (_| || | | | | || _|| (_| || | | | | | | |
// /_/ \_\|_| \___||_| |_||_||____/ \__|\___| \__,_||_| |_| |_||_| \__,_||_| |_| |_| |_|
// |
// 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

@@ -4,7 +4,7 @@
// / ___ \ | | | (__ | | | || | ___) || |_| __/| (_| || | | | | || _|| (_| || | | | | | | |
// /_/ \_\|_| \___||_| |_||_||____/ \__|\___| \__,_||_| |_| |_||_| \__,_||_| |_| |_| |_|
// |
// 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 +20,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 +40,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

@@ -4,7 +4,7 @@
// / ___ \ | | | (__ | | | || | ___) || |_| __/| (_| || | | | | || _|| (_| || | | | | | | |
// /_/ \_\|_| \___||_| |_||_||____/ \__|\___| \__,_||_| |_| |_||_| \__,_||_| |_| |_| |_|
// |
// 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

@@ -4,7 +4,7 @@
// / ___ \ | | | (__ | | | || | ___) || |_| __/| (_| || | | | | || _|| (_| || | | | | | | |
// /_/ \_\|_| \___||_| |_||_||____/ \__|\___| \__,_||_| |_| |_||_| \__,_||_| |_| |_| |_|
// |
// 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,11 +20,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

@@ -4,7 +4,7 @@
// / ___ \ | | | (__ | | | || | ___) || |_| __/| (_| || | | | | || _|| (_| || | | | | | | |
// /_/ \_\|_| \___||_| |_||_||____/ \__|\___| \__,_||_| |_| |_||_| \__,_||_| |_| |_| |_|
// |
// 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,13 +20,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

@@ -4,7 +4,7 @@
// / ___ \ | | | (__ | | | || | ___) || |_| __/| (_| || | | | | || _|| (_| || | | | | | | |
// /_/ \_\|_| \___||_| |_||_||____/ \__|\___| \__,_||_| |_| |_||_| \__,_||_| |_| |_| |_|
// |
// 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

@@ -4,7 +4,7 @@
// / ___ \ | | | (__ | | | || | ___) || |_| __/| (_| || | | | | || _|| (_| || | | | | | | |
// /_/ \_\|_| \___||_| |_||_||____/ \__|\___| \__,_||_| |_| |_||_| \__,_||_| |_| |_| |_|
// |
// 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,7 +20,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 +33,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

@@ -4,7 +4,7 @@
// / ___ \ | | | (__ | | | || | ___) || |_| __/| (_| || | | | | || _|| (_| || | | | | | | |
// /_/ \_\|_| \___||_| |_||_||____/ \__|\___| \__,_||_| |_| |_||_| \__,_||_| |_| |_| |_|
// |
// 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

@@ -4,7 +4,7 @@
// / ___ \ | | | (__ | | | || | ___) || |_| __/| (_| || | | | | || _|| (_| || | | | | | | |
// /_/ \_\|_| \___||_| |_||_||____/ \__|\___| \__,_||_| |_| |_||_| \__,_||_| |_| |_| |_|
// |
// 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 +41,7 @@ using SteamKit2;
namespace ArchiSteamFarm.OfficialPlugins.ItemsMatcher;
internal static class Backend {
internal static async Task<BasicResponse?> AnnounceDiffForListing(WebBrowser webBrowser, ulong steamID, ICollection<AssetForListing> inventory, string inventoryChecksum, IReadOnlyCollection<Asset.EType> acceptedMatchableTypes, uint totalInventoryCount, bool matchEverything, string tradeToken, ICollection<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<Asset.EType> 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) {
@@ -65,14 +65,14 @@ internal static class Backend {
ArgumentNullException.ThrowIfNull(inventoryRemoved);
ArgumentException.ThrowIfNullOrEmpty(previousInventoryChecksum);
Uri request = new(ArchiNet.URL, "/Api/Listing/AnnounceDiff");
Uri request = new(ArchiNet.URL, "/Api/Listing/AnnounceDiff/v2");
AnnouncementDiffRequest data = new(ASF.GlobalDatabase?.Identifier ?? Guid.NewGuid(), steamID, inventory, inventoryChecksum, acceptedMatchableTypes, totalInventoryCount, matchEverything, ASF.GlobalConfig?.MaxTradeHoldDuration ?? GlobalConfig.DefaultMaxTradeHoldDuration, tradeToken, inventoryRemoved, previousInventoryChecksum, nickname, avatarHash);
return await webBrowser.UrlPost(request, data: data, requestOptions: WebBrowser.ERequestOptions.ReturnRedirections | WebBrowser.ERequestOptions.ReturnClientErrors | WebBrowser.ERequestOptions.CompressRequest).ConfigureAwait(false);
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<BasicResponse?> AnnounceForListing(WebBrowser webBrowser, ulong steamID, ICollection<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<Asset.EType> acceptedMatchableTypes, uint totalInventoryCount, bool matchEverything, string tradeToken, string? nickname = null, string? avatarHash = null) {
ArgumentNullException.ThrowIfNull(webBrowser);
if ((steamID == 0) || !new SteamID(steamID).IsIndividualAccount) {
@@ -96,11 +96,11 @@ internal static class Backend {
throw new ArgumentOutOfRangeException(nameof(tradeToken));
}
Uri request = new(ArchiNet.URL, "/Api/Listing/Announce/v4");
Uri request = new(ArchiNet.URL, "/Api/Listing/Announce/v5");
AnnouncementRequest data = new(ASF.GlobalDatabase?.Identifier ?? Guid.NewGuid(), steamID, inventory, inventoryChecksum, acceptedMatchableTypes, totalInventoryCount, matchEverything, ASF.GlobalConfig?.MaxTradeHoldDuration ?? GlobalConfig.DefaultMaxTradeHoldDuration, tradeToken, nickname, avatarHash);
return await webBrowser.UrlPost(request, data: data, requestOptions: WebBrowser.ERequestOptions.ReturnRedirections | WebBrowser.ERequestOptions.ReturnClientErrors | WebBrowser.ERequestOptions.CompressRequest).ConfigureAwait(false);
return await webBrowser.UrlPostToJsonObject<GenericResponse<BackgroundTaskResponse>, AnnouncementRequest>(request, data: data, requestOptions: WebBrowser.ERequestOptions.ReturnRedirections | WebBrowser.ERequestOptions.ReturnClientErrors | WebBrowser.ERequestOptions.AllowInvalidBodyOnErrors | WebBrowser.ERequestOptions.CompressRequest).ConfigureAwait(false);
}
internal static string GenerateChecksumFor(IList<AssetForListing> assetsForListings) {
@@ -114,6 +114,21 @@ internal static class Backend {
return Utilities.GenerateChecksumFor(bytes);
}
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<Asset.EType> acceptedMatchableTypes) {
ArgumentOutOfRangeException.ThrowIfEqual(licenseID, Guid.Empty);
ArgumentNullException.ThrowIfNull(bot);
@@ -144,7 +159,7 @@ internal static class Backend {
return (response.StatusCode, response.Content?.Result ?? ImmutableHashSet<ListedUser>.Empty);
}
internal static async Task<ObjectResponse<GenericResponse<ImmutableHashSet<SetPart>>>?> GetSetParts(WebBrowser webBrowser, ulong steamID, ICollection<Asset.EType> matchableTypes, ICollection<uint> realAppIDs, CancellationToken cancellationToken = default) {
internal static async Task<ObjectResponse<GenericResponse<ImmutableHashSet<SetPart>>>?> GetSetParts(WebBrowser webBrowser, ulong steamID, IReadOnlyCollection<Asset.EType> matchableTypes, IReadOnlyCollection<uint> realAppIDs, CancellationToken cancellationToken = default) {
ArgumentNullException.ThrowIfNull(webBrowser);
if ((steamID == 0) || !new SteamID(steamID).IsIndividualAccount) {
@@ -176,4 +191,22 @@ internal static class Backend {
return await webBrowser.UrlPost(request, data: data, requestOptions: WebBrowser.ERequestOptions.ReturnRedirections | WebBrowser.ERequestOptions.ReturnClientErrors | WebBrowser.ERequestOptions.CompressRequest).ConfigureAwait(false);
}
internal static async Task<ObjectResponse<GenericResponse<BackgroundTaskResponse>>?> PollResult(WebBrowser webBrowser, ulong steamID, Guid requestID) {
ArgumentNullException.ThrowIfNull(webBrowser);
if ((steamID == 0) || !new SteamID(steamID).IsIndividualAccount) {
throw new ArgumentOutOfRangeException(nameof(steamID));
}
ArgumentOutOfRangeException.ThrowIfEqual(requestID, Guid.Empty);
if (SharedInfo.BuildInfo.IsCustomBuild) {
return null;
}
Uri request = new(ArchiNet.URL, $"/Api/Listing/PollResult/{steamID}/{requestID:N}");
return await webBrowser.UrlGetToJsonObject<GenericResponse<BackgroundTaskResponse>>(request, requestOptions: WebBrowser.ERequestOptions.ReturnRedirections | WebBrowser.ERequestOptions.ReturnClientErrors | WebBrowser.ERequestOptions.AllowInvalidBodyOnErrors).ConfigureAwait(false);
}
}

View File

@@ -4,7 +4,7 @@
// / ___ \ | | | (__ | | | || | ___) || |_| __/| (_| || | | | | || _|| (_| || | | | | | | |
// /_/ \_\|_| \___||_| |_||_||____/ \__|\___| \__,_||_| |_| |_||_| \__,_||_| |_| |_| |_|
// |
// 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 +22,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;
@@ -50,8 +52,40 @@ internal sealed class BotCache : SerializableFile {
}
}
[JsonProperty]
private string? BackingLastAnnouncedTradeToken;
internal string? LastInventoryChecksumBeforeDeduplication {
get => BackingLastInventoryChecksumBeforeDeduplication;
set {
if (BackingLastInventoryChecksumBeforeDeduplication == value) {
return;
}
BackingLastInventoryChecksumBeforeDeduplication = value;
Utilities.InBackground(Save);
}
}
internal DateTime? LastRequestAt {
get => BackingLastRequestAt;
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);
@@ -65,6 +99,12 @@ internal sealed class BotCache : SerializableFile {
[UsedImplicitly]
public bool ShouldSerializeBackingLastAnnouncedTradeToken() => !string.IsNullOrEmpty(BackingLastAnnouncedTradeToken);
[UsedImplicitly]
public bool ShouldSerializeBackingLastInventoryChecksumBeforeDeduplication() => !string.IsNullOrEmpty(BackingLastInventoryChecksumBeforeDeduplication);
[UsedImplicitly]
public bool ShouldSerializeBackingLastRequestAt() => BackingLastRequestAt.HasValue;
[UsedImplicitly]
public bool ShouldSerializeLastAnnouncedAssetsForListing() => LastAnnouncedAssetsForListing.Count > 0;
@@ -78,6 +118,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);
@@ -96,7 +138,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

@@ -4,7 +4,7 @@
// / ___ \ | | | (__ | | | || | ___) || |_| __/| (_| || | | | | || _|| (_| || | | | | | | |
// /_/ \_\|_| \___||_| |_||_||____/ \__|\___| \__,_||_| |_| |_||_| \__,_||_| |_| |_| |_|
// |
// 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 +80,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 +120,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

@@ -4,7 +4,7 @@
// / ___ \ | | | (__ | | | || | ___) || |_| __/| (_| || | | | | || _|| (_| || | | | | | | |
// /_/ \_\|_| \___||_| |_||_||____/ \__|\___| \__,_||_| |_| |_||_| \__,_||_| |_| |_| |_|
// |
// 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 +22,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, ICollection<AssetForListing> inventory, string inventoryChecksum, IReadOnlyCollection<Asset.EType> matchableTypes, uint totalInventoryCount, bool matchEverything, byte maxTradeHoldDuration, string tradeToken, ICollection<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<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) {
ArgumentOutOfRangeException.ThrowIfEqual(guid, Guid.Empty);
if ((steamID == 0) || !new SteamID(steamID).IsIndividualAccount) {

View File

@@ -4,7 +4,7 @@
// / ___ \ | | | (__ | | | || | ___) || |_| __/| (_| || | | | | || _|| (_| || | | | | | | |
// /_/ \_\|_| \___||_| |_||_||____/ \__|\___| \__,_||_| |_| |_||_| \__,_||_| |_| |_| |_|
// |
// 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,59 +22,65 @@
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<Asset.EType> 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, ICollection<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<Asset.EType> 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) {
throw new ArgumentOutOfRangeException(nameof(steamID));
}
if ((inventory == null) || (inventory.Count == 0)) {
throw new ArgumentNullException(nameof(inventory));
}
ArgumentNullException.ThrowIfNull(inventory);
ArgumentException.ThrowIfNullOrEmpty(inventoryChecksum);
if ((matchableTypes == null) || (matchableTypes.Count == 0)) {

View File

@@ -4,7 +4,7 @@
// / ___ \ | | | (__ | | | || | ___) || |_| __/| (_| || | | | | || _|| (_| || | | | | | | |
// /_/ \_\|_| \___||_| |_||_||____/ \__|\___| \__,_||_| |_| |_||_| \__,_||_| |_| |_| |_|
// |
// 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 +20,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

@@ -4,7 +4,7 @@
// / ___ \ | | | (__ | | | || | ___) || |_| __/| (_| || | | | | || _|| (_| || | | | | | | |
// /_/ \_\|_| \___||_| |_||_||____/ \__|\___| \__,_||_| |_| |_||_| \__,_||_| |_| |_| |_|
// |
// 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 +20,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 Asset.ERarity 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 Asset.EType Type { get; private init; }
[JsonConstructor]
protected AssetForMatching() { }

View File

@@ -4,7 +4,7 @@
// / ___ \ | | | (__ | | | || | ___) || |_| __/| (_| || | | | | || _|| (_| || | | | | | | |
// /_/ \_\|_| \___||_| |_||_||____/ \__|\___| \__,_||_| |_| |_||_| \__,_||_| |_| |_| |_|
// |
// 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 +20,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() { }

View File

@@ -1,10 +1,10 @@
// _ _ _ ____ _ _____
// _ _ _ ____ _ _____
// / \ _ __ ___ | |__ (_)/ ___| | |_ ___ __ _ _ __ ___ | ___|__ _ _ __ _ __ ___
// / _ \ | '__|/ __|| '_ \ | |\___ \ | __|/ _ \ / _` || '_ ` _ \ | |_ / _` || '__|| '_ ` _ \
// / ___ \ | | | (__ | | | || | ___) || |_| __/| (_| || | | | | || _|| (_| || | | | | | | |
// /_/ \_\|_| \___||_| |_||_||____/ \__|\___| \__,_||_| |_| |_||_| \__,_||_| |_| |_| |_|
// |
// 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,25 +19,24 @@
// See the License for the specific language governing permissions and
// limitations under the License.
using System;
using System.Diagnostics.CodeAnalysis;
using Newtonsoft.Json;
using System.Text.Json.Serialization;
namespace ArchiSteamFarm.Steam.Data;
namespace ArchiSteamFarm.OfficialPlugins.ItemsMatcher.Data;
#pragma warning disable CA1812 // False positive, the class is used during json deserialization
[SuppressMessage("ReSharper", "ClassCannotBeInstantiated")]
internal sealed class AccessTokenResponse : ResultResponse {
[JsonProperty("data", Required = Required.Always)]
internal readonly AccessTokenData Data = new();
internal sealed class BackgroundTaskResponse {
[JsonInclude]
[JsonRequired]
internal bool Finished { get; private init; }
[JsonInclude]
[JsonRequired]
internal Guid RequestID { get; private init; }
[JsonConstructor]
private AccessTokenResponse() { }
[SuppressMessage("ReSharper", "ClassCannotBeInstantiated")]
internal sealed class AccessTokenData {
[JsonProperty("webapi_token", Required = Required.Always)]
internal readonly string WebAPIToken = "";
[JsonConstructor]
internal AccessTokenData() { }
}
private BackgroundTaskResponse() { }
}
#pragma warning restore CA1812 // False positive, the class is used during json deserialization

View File

@@ -4,7 +4,7 @@
// / ___ \ | | | (__ | | | || | ___) || |_| __/| (_| || | | | | || _|| (_| || | | | | | | |
// /_/ \_\|_| \___||_| |_||_||____/ \__|\___| \__,_||_| |_| |_||_| \__,_||_| |_| |_| |_|
// |
// 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 +20,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

@@ -4,7 +4,7 @@
// / ___ \ | | | (__ | | | || | ___) || |_| __/| (_| || | | | | || _|| (_| || | | | | | | |
// /_/ \_\|_| \___||_| |_||_||____/ \__|\___| \__,_||_| |_| |_||_| \__,_||_| |_| |_| |_|
// |
// 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,24 +23,28 @@ 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<Asset.EType> 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) {
ArgumentOutOfRangeException.ThrowIfEqual(guid, Guid.Empty);

View File

@@ -4,7 +4,7 @@
// / ___ \ | | | (__ | | | || | ___) || |_| __/| (_| || | | | | || _|| (_| || | | | | | | |
// /_/ \_\|_| \___||_| |_||_||____/ \__|\___| \__,_||_| |_| |_||_| \__,_||_| |_| |_| |_|
// |
// 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 +21,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; } = ImmutableHashSet<AssetInInventory>.Empty;
[JsonProperty(Required = Required.Always)]
internal readonly ImmutableHashSet<Asset.EType> MatchableTypes = ImmutableHashSet<Asset.EType>.Empty;
[JsonInclude]
[JsonRequired]
internal ImmutableHashSet<Asset.EType> MatchableTypes { get; private init; } = ImmutableHashSet<Asset.EType>.Empty;
#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

@@ -4,7 +4,7 @@
// / ___ \ | | | (__ | | | || | ___) || |_| __/| (_| || | | | | || _|| (_| || | | | | | | |
// /_/ \_\|_| \___||_| |_||_||____/ \__|\___| \__,_||_| |_| |_||_| \__,_||_| |_| |_| |_|
// |
// 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 +20,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 Asset.ERarity 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 Asset.EType Type { get; private init; }
[JsonConstructor]
private SetPart() { }

View File

@@ -4,7 +4,7 @@
// / ___ \ | | | (__ | | | || | ___) || |_| __/| (_| || | | | | || _|| (_| || | | | | | | |
// /_/ \_\|_| \___||_| |_||_||____/ \__|\___| \__,_||_| |_| |_||_| \__,_||_| |_| |_| |_|
// |
// 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 +22,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<Asset.EType> 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, ICollection<Asset.EType> matchableTypes, ICollection<uint> realAppIDs) {
internal SetPartsRequest(Guid guid, ulong steamID, IReadOnlyCollection<Asset.EType> matchableTypes, IReadOnlyCollection<uint> realAppIDs) {
ArgumentOutOfRangeException.ThrowIfEqual(guid, Guid.Empty);
if ((steamID == 0) || !new SteamID(steamID).IsIndividualAccount) {

View File

@@ -4,7 +4,7 @@
// / ___ \ | | | (__ | | | || | ___) || |_| __/| (_| || | | | | || _|| (_| || | | | | | | |
// /_/ \_\|_| \___||_| |_||_||____/ \__|\___| \__,_||_| |_| |_||_| \__,_||_| |_| |_| |_|
// |
// 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 +23,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 +36,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 +44,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 +86,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

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

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

@@ -4,7 +4,7 @@
// / ___ \ | | | (__ | | | || | ___) || |_| __/| (_| || | | | | || _|| (_| || | | | | | | |
// /_/ \_\|_| \___||_| |_||_||____/ \__|\___| \__,_||_| |_| |_||_| \__,_||_| |_| |_| |_|
// |
// 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 +20,7 @@
// limitations under the License.
using System;
using System.Collections.Frozen;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Globalization;
@@ -27,10 +28,10 @@ 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;
using ArchiSteamFarm.Helpers;
using ArchiSteamFarm.IPC.Responses;
using ArchiSteamFarm.Localization;
using ArchiSteamFarm.OfficialPlugins.ItemsMatcher.Data;
@@ -43,7 +44,6 @@ using ArchiSteamFarm.Steam.Storage;
using ArchiSteamFarm.Storage;
using ArchiSteamFarm.Web;
using ArchiSteamFarm.Web.Responses;
using Newtonsoft.Json.Linq;
using SteamKit2;
using SteamKit2.Internal;
@@ -52,6 +52,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
@@ -60,12 +61,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(
private static readonly FrozenSet<Asset.EType> AcceptedMatchableTypes = new HashSet<Asset.EType>(4) {
Asset.EType.Emoticon,
Asset.EType.FoilTradingCard,
Asset.EType.ProfileBackground,
Asset.EType.TradingCard
);
}.ToFrozenSet();
private readonly Bot Bot;
private readonly Timer? HeartBeatTimer;
@@ -279,12 +280,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();
foreach (Asset item in inventory) {
if (item is { AssetID: > 0, Amount: > 0, ClassID: > 0, RealAppID: > 0, Type: > Asset.EType.Unknown, Rarity: > Asset.ERarity.Unknown } && acceptedMatchableTypes.Contains(item.Type)) {
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)) {
// Only tradable assets matter for MatchEverything bots
if (!matchEverything || item.Tradable) {
assetsForListing.Add(new AssetForListing(item, index, previousAssetID));
@@ -329,6 +330,27 @@ internal sealed class RemoteCommunication : IAsyncDisposable, IDisposable {
}
}
BotCache ??= await BotCache.CreateOrLoad(BotCacheFilePath).ConfigureAwait(false);
string inventoryChecksumBeforeDeduplication = Backend.GenerateChecksumFor(assetsForListing);
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;
LastAnnouncement = DateTime.UtcNow;
ShouldSendAnnouncementEarlier = false;
ShouldSendHeartBeats = true;
if (triggerImmediately) {
Utilities.InBackground(() => OnHeartBeatTimer());
}
return;
}
}
if (!SignedInWithSteam) {
HttpStatusCode? signInWithSteam = await ArchiNet.SignInWithSteam(Bot, WebBrowser).ConfigureAwait(false);
@@ -350,11 +372,9 @@ internal sealed class RemoteCommunication : IAsyncDisposable, IDisposable {
SignedInWithSteam = true;
}
BotCache ??= await BotCache.CreateOrLoad(BotCacheFilePath).ConfigureAwait(false);
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();
HashSet<uint> realAppIDs = [];
Dictionary<(uint RealAppID, Asset.EType Type, Asset.ERarity Rarity), Dictionary<ulong, uint>> state = new();
foreach (AssetForListing asset in assetsForListing) {
@@ -371,13 +391,64 @@ internal sealed class RemoteCommunication : IAsyncDisposable, IDisposable {
ObjectResponse<GenericResponse<ImmutableHashSet<SetPart>>>? setPartsResponse = await Backend.GetSetParts(WebBrowser, Bot.SteamID, acceptedMatchableTypes, realAppIDs).ConfigureAwait(false);
if (!HandleAnnounceResponse(BotCache, tradeToken, response: setPartsResponse) || (setPartsResponse?.Content?.Result == null)) {
if (setPartsResponse == null) {
// This is actually a network failure, so we'll stop sending heartbeats but not record it as valid check
ShouldSendHeartBeats = false;
Bot.ArchiLogger.LogGenericWarning(string.Format(CultureInfo.CurrentCulture, Strings.ErrorObjectIsNull, nameof(setPartsResponse)));
return;
}
if (setPartsResponse.StatusCode.IsRedirectionCode()) {
ShouldSendHeartBeats = false;
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;
}
if (!setPartsResponse.StatusCode.IsSuccessCode()) {
// ArchiNet told us that we've sent a bad request, so the process should restart from the beginning at later time
ShouldSendHeartBeats = false;
Bot.ArchiLogger.LogGenericWarning(string.Format(CultureInfo.CurrentCulture, Strings.WarningFailedWithError, setPartsResponse.StatusCode));
switch (setPartsResponse.StatusCode) {
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;
}
}
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, 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());
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) {
if (!databaseSets.TryGetValue(key, out HashSet<ulong>? databaseSet)) {
@@ -400,7 +471,7 @@ internal sealed class RemoteCommunication : IAsyncDisposable, IDisposable {
minimumAmount = amount;
}
setCopy.Add((classID, amount));
setCopy[classID] = amount;
}
foreach ((ulong classID, uint amount) in setCopy) {
@@ -414,7 +485,7 @@ 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);
@@ -444,6 +515,9 @@ internal sealed class RemoteCommunication : IAsyncDisposable, IDisposable {
LastAnnouncement = DateTime.UtcNow;
ShouldSendAnnouncementEarlier = ShouldSendHeartBeats = false;
// There is a possibility that our inventory has changed even if our announced assets did not, record that
BotCache.LastInventoryChecksumBeforeDeduplication = inventoryChecksumBeforeDeduplication;
return;
}
}
@@ -453,6 +527,9 @@ internal sealed class RemoteCommunication : IAsyncDisposable, IDisposable {
LastAnnouncement = DateTime.UtcNow;
ShouldSendAnnouncementEarlier = ShouldSendHeartBeats = false;
// There is a possibility that our inventory has changed even if our announced assets did not, record that
BotCache.LastInventoryChecksumBeforeDeduplication = inventoryChecksumBeforeDeduplication;
Bot.ArchiLogger.LogGenericWarning(string.Format(CultureInfo.CurrentCulture, Strings.WarningFailedWithError, $"{nameof(assetsForListing)} > {MaxItemsCount}"));
return;
@@ -461,13 +538,20 @@ 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;
LastAnnouncement = DateTime.UtcNow;
ShouldSendAnnouncementEarlier = false;
ShouldSendHeartBeats = true;
if (triggerImmediately) {
Utilities.InBackground(() => OnHeartBeatTimer());
}
// There is a possibility that our inventory has changed even if our announced assets did not, record that
BotCache.LastInventoryChecksumBeforeDeduplication = inventoryChecksumBeforeDeduplication;
return;
}
@@ -475,124 +559,233 @@ internal sealed class RemoteCommunication : IAsyncDisposable, IDisposable {
if (BotCache.LastAnnouncedAssetsForListing.Count > 0) {
Dictionary<ulong, AssetForListing> previousInventoryState = BotCache.LastAnnouncedAssetsForListing.ToDictionary(static asset => asset.AssetID);
HashSet<AssetForListing> inventoryAdded = new();
HashSet<AssetForListing> inventoryRemoved = new();
foreach (AssetForListing asset in assetsForListing) {
if (previousInventoryState.Remove(asset.AssetID, out AssetForListing? previousAsset) && (asset.BackendHashCode == previousAsset.BackendHashCode)) {
continue;
}
inventoryAdded.Add(asset);
}
foreach (AssetForListing asset in previousInventoryState.Values) {
inventoryRemoved.Add(asset);
}
HashSet<AssetForListing> inventoryAddedChanged = assetsForListing.Where(asset => !previousInventoryState.Remove(asset.AssetID, out AssetForListing? previousAsset) || (asset.BackendHashCode != previousAsset.BackendHashCode)).ToHashSet();
Bot.ArchiLogger.LogGenericInfo(string.Format(CultureInfo.CurrentCulture, Localization.Strings.ListingAnnouncing, Bot.SteamID, nickname ?? Bot.SteamID.ToString(CultureInfo.InvariantCulture), assetsForListing.Count));
BasicResponse? diffResponse = await Backend.AnnounceDiffForListing(WebBrowser, Bot.SteamID, inventoryAdded, checksum, acceptedMatchableTypes, (uint) inventory.Count, matchEverything, tradeToken, inventoryRemoved, previousChecksum, nickname, avatarHash).ConfigureAwait(false);
ObjectResponse<GenericResponse<BackgroundTaskResponse>>? diffResponse = null;
Guid diffRequestID = Guid.Empty;
if (HandleAnnounceResponse(BotCache, tradeToken, previousChecksum, assetsForListing, diffResponse)) {
for (byte i = 0; i < WebBrowser.MaxTries; i++) {
if (diffRequestID != Guid.Empty) {
diffResponse = await Backend.PollResult(WebBrowser, Bot.SteamID, diffRequestID).ConfigureAwait(false);
} else {
diffResponse = await Backend.AnnounceDiffForListing(WebBrowser, Bot.SteamID, inventoryAddedChanged, checksum, acceptedMatchableTypes, (uint) inventory.Count, matchEverything, tradeToken, previousInventoryState.Values, previousChecksum, nickname, avatarHash).ConfigureAwait(false);
}
if (diffResponse == null) {
// This is actually a network failure, so we'll stop sending heartbeats but not record it as valid check
ShouldSendHeartBeats = false;
Bot.ArchiLogger.LogGenericWarning(string.Format(CultureInfo.CurrentCulture, Strings.ErrorObjectIsNull, nameof(diffResponse)));
return;
}
if (diffResponse.StatusCode.IsRedirectionCode()) {
ShouldSendHeartBeats = false;
Bot.ArchiLogger.LogGenericWarning(string.Format(CultureInfo.CurrentCulture, Strings.WarningFailedWithError, diffResponse.StatusCode));
if (diffResponse.FinalUri.Host != ArchiWebHandler.SteamCommunityURL.Host) {
ASF.ArchiLogger.LogGenericError(string.Format(CultureInfo.CurrentCulture, Strings.WarningUnknownValuePleaseReport, nameof(diffResponse.FinalUri), diffResponse.FinalUri));
return;
}
// We've expected the result, not the redirection to the sign in, we need to authenticate again
SignedInWithSteam = false;
return;
}
if (!diffResponse.StatusCode.IsSuccessCode()) {
// ArchiNet told us that we've sent a bad request, so the process should restart from the beginning at later time
ShouldSendHeartBeats = false;
Bot.ArchiLogger.LogGenericWarning(string.Format(CultureInfo.CurrentCulture, Strings.WarningFailedWithError, diffResponse.StatusCode));
switch (diffResponse.StatusCode) {
case HttpStatusCode.Conflict:
// ArchiNet told us to do full announcement instead, the only non-OK response we accept
break;
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;
}
break;
}
// Great, do we need to wait?
if (diffResponse.Content?.Result == null) {
// This should never happen if we got the correct response
Bot.ArchiLogger.LogGenericError(string.Format(CultureInfo.CurrentCulture, Strings.WarningUnknownValuePleaseReport, nameof(diffResponse), diffResponse.Content?.Result));
return;
}
if (diffResponse.Content.Result.Finished) {
break;
}
diffRequestID = diffResponse.Content.Result.RequestID;
diffResponse = null;
}
if (diffResponse == null) {
// We've waited long enough, something is definitely wrong with us or the backend
Bot.ArchiLogger.LogGenericWarning(string.Format(CultureInfo.CurrentCulture, Strings.WarningFailedWithError, nameof(diffResponse)));
return;
}
if (diffResponse.StatusCode.IsSuccessCode() && diffResponse.Content is { Success: true, Result.Finished: true }) {
// Our diff announce has succeeded, we have nothing to do further
Bot.ArchiLogger.LogGenericInfo(Strings.Success);
LastAnnouncement = LastHeartBeat = DateTime.UtcNow;
ShouldSendAnnouncementEarlier = false;
ShouldSendHeartBeats = true;
BotCache.LastAnnouncedAssetsForListing.ReplaceWith(assetsForListing);
BotCache.LastAnnouncedTradeToken = tradeToken;
BotCache.LastInventoryChecksumBeforeDeduplication = inventoryChecksumBeforeDeduplication;
BotCache.LastRequestAt = LastHeartBeat;
return;
}
}
Bot.ArchiLogger.LogGenericInfo(string.Format(CultureInfo.CurrentCulture, Localization.Strings.ListingAnnouncing, Bot.SteamID, nickname ?? Bot.SteamID.ToString(CultureInfo.InvariantCulture), assetsForListing.Count));
BasicResponse? response = await Backend.AnnounceForListing(WebBrowser, Bot.SteamID, assetsForListing, checksum, acceptedMatchableTypes, (uint) inventory.Count, matchEverything, tradeToken, nickname, avatarHash).ConfigureAwait(false);
ObjectResponse<GenericResponse<BackgroundTaskResponse>>? announceResponse = null;
Guid announceRequestID = Guid.Empty;
HandleAnnounceResponse(BotCache, tradeToken, assetsForListing: assetsForListing, response: response);
} finally {
RequestsSemaphore.Release();
for (byte i = 0; i < WebBrowser.MaxTries; i++) {
if (announceRequestID != Guid.Empty) {
announceResponse = await Backend.PollResult(WebBrowser, Bot.SteamID, announceRequestID).ConfigureAwait(false);
} else {
announceResponse = await Backend.AnnounceForListing(WebBrowser, Bot.SteamID, assetsForListing, checksum, acceptedMatchableTypes, (uint) inventory.Count, matchEverything, tradeToken, nickname, avatarHash).ConfigureAwait(false);
}
Bot.ArchiLogger.LogGenericInfo(Strings.Success);
}
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));
}
}
private bool HandleAnnounceResponse(BotCache botCache, string tradeToken, string? previousInventoryChecksum = null, ICollection<AssetForListing>? assetsForListing = null, BasicResponse? response = null) {
ArgumentNullException.ThrowIfNull(botCache);
ArgumentException.ThrowIfNullOrEmpty(tradeToken);
if (response == null) {
if (announceResponse == null) {
// This is actually a network failure, so we'll stop sending heartbeats but not record it as valid check
ShouldSendHeartBeats = false;
Bot.ArchiLogger.LogGenericWarning(string.Format(CultureInfo.CurrentCulture, Strings.ErrorObjectIsNull, nameof(announceResponse)));
Bot.ArchiLogger.LogGenericWarning(string.Format(CultureInfo.CurrentCulture, Strings.ErrorObjectIsNull, nameof(response)));
return false;
return;
}
if (response.StatusCode.IsRedirectionCode()) {
if (announceResponse.StatusCode.IsRedirectionCode()) {
ShouldSendHeartBeats = false;
Bot.ArchiLogger.LogGenericWarning(string.Format(CultureInfo.CurrentCulture, Strings.WarningFailedWithError, announceResponse.StatusCode));
Bot.ArchiLogger.LogGenericWarning(string.Format(CultureInfo.CurrentCulture, Strings.WarningFailedWithError, response.StatusCode));
if (announceResponse.FinalUri.Host != ArchiWebHandler.SteamCommunityURL.Host) {
ASF.ArchiLogger.LogGenericError(string.Format(CultureInfo.CurrentCulture, Strings.WarningUnknownValuePleaseReport, nameof(announceResponse.FinalUri), announceResponse.FinalUri));
if (response.FinalUri.Host != ArchiWebHandler.SteamCommunityURL.Host) {
ASF.ArchiLogger.LogGenericError(string.Format(CultureInfo.CurrentCulture, Strings.WarningUnknownValuePleaseReport, nameof(response.FinalUri), response.FinalUri));
return false;
return;
}
// We've expected the result, not the redirection to the sign in, we need to authenticate again
SignedInWithSteam = false;
return false;
return;
}
if (response.StatusCode.IsClientErrorCode()) {
if (!announceResponse.StatusCode.IsSuccessCode()) {
// ArchiNet told us that we've sent a bad request, so the process should restart from the beginning at later time
ShouldSendHeartBeats = false;
Bot.ArchiLogger.LogGenericWarning(string.Format(CultureInfo.CurrentCulture, Strings.WarningFailedWithError, announceResponse.StatusCode));
Bot.ArchiLogger.LogGenericWarning(string.Format(CultureInfo.CurrentCulture, Strings.WarningFailedWithError, response.StatusCode));
switch (announceResponse.StatusCode) {
case HttpStatusCode.Conflict:
// ArchiNet told us to that we've applied wrong deduplication logic, we can try again in a second
LastAnnouncement = DateTime.UtcNow.AddMinutes(5);
switch (response.StatusCode) {
case HttpStatusCode.Conflict when !string.IsNullOrEmpty(previousInventoryChecksum):
// ArchiNet told us to do full announcement instead
return false;
return;
case HttpStatusCode.Forbidden:
// ArchiNet told us to stop submitting data for now
LastAnnouncement = DateTime.UtcNow.AddYears(1);
return false;
return;
case HttpStatusCode.TooManyRequests:
// ArchiNet told us to try again later
LastAnnouncement = DateTime.UtcNow.AddDays(1);
return false;
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 false;
return;
}
}
if (assetsForListing?.Count > 0) {
// Great, do we need to wait?
if (announceResponse.Content?.Result == null) {
// This should never happen if we got the correct response
Bot.ArchiLogger.LogGenericError(string.Format(CultureInfo.CurrentCulture, Strings.WarningUnknownValuePleaseReport, nameof(announceResponse), announceResponse.Content?.Result));
return;
}
if (announceResponse.Content.Result.Finished) {
break;
}
announceRequestID = announceResponse.Content.Result.RequestID;
announceResponse = null;
}
if (announceResponse == null) {
// We've waited long enough, something is definitely wrong with us or the backend
Bot.ArchiLogger.LogGenericWarning(string.Format(CultureInfo.CurrentCulture, Strings.WarningFailedWithError, nameof(announceResponse)));
return;
}
if (announceResponse.StatusCode.IsSuccessCode() && announceResponse.Content is { Success: true, Result.Finished: true }) {
// Our diff announce has succeeded, we have nothing to do further
Bot.ArchiLogger.LogGenericInfo(Strings.Success);
LastAnnouncement = LastHeartBeat = DateTime.UtcNow;
ShouldSendAnnouncementEarlier = false;
ShouldSendHeartBeats = true;
botCache.LastAnnouncedAssetsForListing.ReplaceWith(assetsForListing);
botCache.LastAnnouncedTradeToken = tradeToken;
BotCache.LastAnnouncedAssetsForListing.ReplaceWith(assetsForListing);
BotCache.LastAnnouncedTradeToken = tradeToken;
BotCache.LastInventoryChecksumBeforeDeduplication = inventoryChecksumBeforeDeduplication;
BotCache.LastRequestAt = LastHeartBeat;
return;
}
return true;
// Everything we've tried has failed
Bot.ArchiLogger.LogGenericWarning(Strings.WarningFailed);
} finally {
RequestsSemaphore.Release();
}
}
internal void TriggerMatchActivelyEarlier() {
if (MatchActivelyTimer == null) {
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));
}
}
}
private async Task<bool?> IsEligibleForListing() {
@@ -682,24 +875,6 @@ internal sealed class RemoteCommunication : IAsyncDisposable, IDisposable {
return false;
}
// Bot must have valid API key (e.g. not being restricted account)
bool? hasValidApiKey = await Bot.ArchiWebHandler.HasValidApiKey().ConfigureAwait(false);
if (hasValidApiKey != true) {
Bot.ArchiLogger.LogGenericTrace(string.Format(CultureInfo.CurrentCulture, Strings.WarningFailedWithError, $"{nameof(Bot.ArchiWebHandler.HasValidApiKey)}: {hasValidApiKey?.ToString() ?? "null"}"));
return hasValidApiKey.HasValue ? false : null;
}
// Bot can't be trade banned
(bool _, bool? Result) economyBan = await Bot.ArchiWebHandler.CachedEconomyBan.GetValue(ECacheFallback.SuccessPreviously).ConfigureAwait(false);
if (economyBan.Result != false) {
Bot.ArchiLogger.LogGenericTrace(string.Format(CultureInfo.CurrentCulture, Strings.WarningFailedWithError, $"{nameof(economyBan)}: {economyBan.Result?.ToString() ?? "null"}"));
return economyBan.Result.HasValue ? false : null;
}
return true;
}
@@ -712,7 +887,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;
@@ -745,50 +920,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 } && 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.ArchiWebHandler.GetInventoryAsync().Where(item => item is { AssetID: > 0, Amount: > 0, ClassID: > 0, RealAppID: > 0, Type: > Asset.EType.Unknown, Rarity: > Asset.ERarity.Unknown, IsSteamPointsShopItem: false } && acceptedMatchableTypes.Contains(item.Type) && !Bot.BotDatabase.MatchActivelyBlacklistAppIDs.Contains(item.RealAppID)).ToHashSetAsync().ConfigureAwait(false);
} 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, Asset.EType Type, Asset.ERarity Rarity)> setsToKeep = Trading.GetInventorySets(assetsForMatching).Where(static set => set.Value.Any(static amount => amount > 1)).Select(static set => set.Key).ToHashSet();
HashSet<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, Asset.EType Type, Asset.ERarity Rarity), Dictionary<ulong, uint>> setsState = new();
foreach (Asset asset in assetsForMatching) {
realAppIDs.Add(asset.RealAppID);
(uint RealAppID, Asset.EType Type, Asset.ERarity Rarity) key = (asset.RealAppID, asset.Type, asset.Rarity);
if (setsState.TryGetValue(key, out Dictionary<ulong, uint>? set)) {
set[asset.ClassID] = set.TryGetValue(asset.ClassID, out uint amount) ? amount + asset.Amount : 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;
}
(HttpStatusCode StatusCode, ImmutableHashSet<ListedUser> Users)? response = await Backend.GetListedUsersForMatching(ASF.GlobalConfig.LicenseID.Value, Bot, WebBrowser, ourInventory.Values, acceptedMatchableTypes).ConfigureAwait(false);
// We've expected the result, not the redirection to the sign in, we need to authenticate again
SignedInWithSteam = false;
return;
}
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, 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<ulong, uint> setCopy = [];
foreach (((uint RealAppID, Asset.EType Type, Asset.ERarity 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, Asset.EType Type, Asset.ERarity 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)));
@@ -811,9 +1151,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);
@@ -827,20 +1165,20 @@ 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<Asset.EType> 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, Asset.EType Type, Asset.ERarity Rarity), Dictionary<ulong, uint>> ourFullState, Dictionary<(uint RealAppID, Asset.EType Type, Asset.ERarity Rarity), Dictionary<ulong, uint>> ourTradableState) = Trading.GetDividedInventoryState(ourAssets);
if (Trading.IsEmptyForMatching(ourFullState, ourTradableState)) {
// User doesn't have any more dupes in the inventory
@@ -852,26 +1190,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);
@@ -885,7 +1231,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);
}
@@ -893,14 +1239,16 @@ 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)) {
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}"));
@@ -940,13 +1288,13 @@ internal sealed class RemoteCommunication : IAsyncDisposable, IDisposable {
continue;
}
HashSet<(uint RealAppID, Asset.EType Type, Asset.ERarity Rarity)> skippedSetsThisUser = new();
HashSet<(uint RealAppID, Asset.EType Type, Asset.ERarity Rarity)> skippedSetsThisUser = [];
Dictionary<(uint RealAppID, Asset.EType Type, Asset.ERarity Rarity), Dictionary<ulong, uint>> theirTradableState = Trading.GetTradableInventoryState(theirInventory);
for (byte i = 0; i < Trading.MaxTradesPerAccount; i++) {
byte itemsInTrade = 0;
HashSet<(uint RealAppID, Asset.EType Type, Asset.ERarity Rarity)> skippedSetsThisTrade = new();
HashSet<(uint RealAppID, Asset.EType Type, Asset.ERarity Rarity)> skippedSetsThisTrade = [];
Dictionary<ulong, uint> classIDsToGive = new();
Dictionary<ulong, uint> classIDsToReceive = new();
@@ -1087,7 +1435,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) {
@@ -1267,15 +1615,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));
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

@@ -4,7 +4,7 @@
// / ___ \ | | | (__ | | | || | ___) || |_| __/| (_| || | | | | || _|| (_| || | | | | | | |
// /_/ \_\|_| \___||_| |_||_||____/ \__|\___| \__,_||_| |_| |_||_| \__,_||_| |_| |_| |_|
// |
// 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

@@ -4,7 +4,7 @@
// / ___ \ | | | (__ | | | || | ___) || |_| __/| (_| || | | | | || _|| (_| || | | | | | | |
// /_/ \_\|_| \___||_| |_||_||____/ \__|\___| \__,_||_| |_| |_||_| \__,_||_| |_| |_| |_|
// |
// 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,9 +27,9 @@ 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;
@@ -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)));
@@ -220,7 +220,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 +261,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 +313,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;
}
@@ -357,10 +357,10 @@ internal static class Commands {
return bot.Commands.FormatBotResponse(string.Format(CultureInfo.CurrentCulture, Strings.WarningFailedWithError, result));
}
MaFileData maFileData = new(response, deviceID);
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 +392,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

@@ -4,7 +4,7 @@
// / ___ \ | | | (__ | | | || | ___) || |_| __/| (_| || | | | | || _|| (_| || | | | | | | |
// /_/ \_\|_| \___||_| |_||_||____/ \__|\___| \__,_||_| |_| |_||_| \__,_||_| |_| |_| |_|
// |
// 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,47 +20,79 @@
// 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("shared_secret", Required = Required.Always)]
internal readonly string SharedSecret;
[JsonInclude]
[JsonRequired]
internal MaFileSessionData Session { get; private init; }
[JsonProperty("status", Required = Required.Always)]
internal readonly int Status;
[JsonInclude]
[JsonPropertyName("shared_secret")]
[JsonRequired]
internal string SharedSecret { get; private init; }
[JsonProperty("token_gid", Required = Required.Always)]
internal readonly string TokenGid;
[JsonInclude]
[JsonPropertyName("status")]
[JsonRequired]
internal int Status { get; private init; }
[JsonProperty("uri", Required = Required.Always)]
internal readonly string Uri;
[JsonInclude]
[JsonPropertyName("token_gid")]
[JsonRequired]
internal string TokenGid { get; private init; }
internal MaFileData(CTwoFactor_AddAuthenticator_Response data, string deviceID) {
[JsonInclude]
[JsonPropertyName("uri")]
[JsonRequired]
internal string Uri { get; private init; }
internal MaFileData(CTwoFactor_AddAuthenticator_Response data, ulong steamID, string deviceID) {
ArgumentNullException.ThrowIfNull(data);
if ((steamID == 0) || !new SteamID(steamID).IsIndividualAccount) {
throw new ArgumentOutOfRangeException(nameof(steamID));
}
ArgumentException.ThrowIfNullOrEmpty(deviceID);
AccountName = data.account_name;
@@ -70,6 +102,7 @@ internal sealed class MaFileData {
Secret1 = Convert.ToBase64String(data.secret_1);
SerialNumber = data.serial_number;
ServerTime = data.server_time;
Session = new MaFileSessionData(steamID);
SharedSecret = Convert.ToBase64String(data.shared_secret);
Status = data.status;
TokenGid = data.token_gid;

View File

@@ -0,0 +1,40 @@
// _ _ _ ____ _ _____
// / \ _ __ ___ | |__ (_)/ ___| | |_ ___ __ _ _ __ ___ | ___|__ _ _ __ _ __ ___
// / _ \ | '__|/ __|| '_ \ | |\___ \ | __|/ _ \ / _` || '_ ` _ \ | |_ / _` || '__|| '_ ` _ \
// / ___ \ | | | (__ | | | || | ___) || |_| __/| (_| || | | | | || _|| (_| || | | | | | | |
// /_/ \_\|_| \___||_| |_||_||____/ \__|\___| \__,_||_| |_| |_||_| \__,_||_| |_| |_| |_|
// |
// Copyright 2015-2024 Łukasz "JustArchi" Domeradzki
// Contact: JustArchi@JustArchi.net
// |
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
// |
// http://www.apache.org/licenses/LICENSE-2.0
// |
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
using System;
using System.Text.Json.Serialization;
using SteamKit2;
namespace ArchiSteamFarm.OfficialPlugins.MobileAuthenticator;
internal sealed class MaFileSessionData {
[JsonInclude]
[JsonRequired]
internal ulong SteamID { get; private init; }
internal MaFileSessionData(ulong steamID) {
if ((steamID == 0) || !new SteamID(steamID).IsIndividualAccount) {
throw new ArgumentOutOfRangeException(nameof(steamID));
}
SteamID = steamID;
}
}

View File

@@ -4,7 +4,7 @@
// / ___ \ | | | (__ | | | || | ___) || |_| __/| (_| || | | | | || _|| (_| || | | | | | | |
// /_/ \_\|_| \___||_| |_||_||____/ \__|\___| \__,_||_| |_| |_||_| \__,_||_| |_| |_| |_|
// |
// 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

@@ -4,7 +4,7 @@
// / ___ \ | | | (__ | | | || | ___) || |_| __/| (_| || | | | | || _|| (_| || | | | | | | |
// /_/ \_\|_| \___||_| |_||_||____/ \__|\___| \__,_||_| |_| |_||_| \__,_||_| |_| |_| |_|
// |
// 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 +22,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 +39,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

@@ -4,7 +4,7 @@
// / ___ \ | | | (__ | | | || | ___) || |_| __/| (_| || | | | | || _|| (_| || | | | | | | |
// /_/ \_\|_| \___||_| |_||_||____/ \__|\___| \__,_||_| |_| |_||_| \__,_||_| |_| |_| |_|
// |
// 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

@@ -4,7 +4,7 @@
// / ___ \ | | | (__ | | | || | ___) || |_| __/| (_| || | | | | || _|| (_| || | | | | | | |
// /_/ \_\|_| \___||_| |_||_||____/ \__|\___| \__,_||_| |_| |_||_| \__,_||_| |_| |_| |_|
// |
// 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 +23,46 @@ 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;
[JsonInclude]
[JsonPropertyName("guid")]
private string Guid => ASF.GlobalDatabase?.Identifier.ToString("N") ?? throw new InvalidOperationException(nameof(ASF.GlobalDatabase.Identifier));
[JsonProperty("steamid", Required = Required.Always)]
[JsonInclude]
[JsonPropertyName("steamid")]
private string SteamIDText => new SteamID(SteamID).Render();
[JsonInclude]
[JsonPropertyName("token")]
private string Token => SharedInfo.Token;
[JsonInclude]
[JsonPropertyName("v")]
private byte Version => SharedInfo.ApiVersion;
[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

@@ -4,7 +4,7 @@
// / ___ \ | | | (__ | | | || | ___) || |_| __/| (_| || | | | | || _|| (_| || | | | | | | |
// /_/ \_\|_| \___||_| |_||_||____/ \__|\___| \__,_||_| |_| |_||_| \__,_||_| |_| |_| |_|
// |
// 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 +20,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

@@ -4,7 +4,7 @@
// / ___ \ | | | (__ | | | || | ___) || |_| __/| (_| || | | | | || _|| (_| || | | | | | | |
// /_/ \_\|_| \___||_| |_||_||____/ \__|\___| \__,_||_| |_| |_||_| \__,_||_| |_| |_| |_|
// |
// 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 +20,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; } = ImmutableHashSet<uint>.Empty;
[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; } = ImmutableHashSet<uint>.Empty;
[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; } = ImmutableHashSet<uint>.Empty;
[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; } = ImmutableHashSet<uint>.Empty;
[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; } = ImmutableHashSet<uint>.Empty;
[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; } = ImmutableHashSet<uint>.Empty;
}
#pragma warning restore CA1812 // False positive, the class is used during json deserialization

View File

@@ -4,7 +4,7 @@
// / ___ \ | | | (__ | | | || | ___) || |_| __/| (_| || | | | | || _|| (_| || | | | | | | |
// /_/ \_\|_| \___||_| |_||_||____/ \__|\___| \__,_||_| |_| |_||_| \__,_||_| |_| |_| |_|
// |
// 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,52 +21,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> PackageTokens = 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]
@@ -81,9 +86,6 @@ internal sealed class GlobalCache : SerializableFile {
[UsedImplicitly]
public bool ShouldSerializeLastChangeNumber() => LastChangeNumber > 0;
[UsedImplicitly]
public bool ShouldSerializePackageTokens() => !PackageTokens.IsEmpty;
[UsedImplicitly]
public bool ShouldSerializeSubmittedApps() => !SubmittedApps.IsEmpty;
@@ -93,11 +95,20 @@ 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) == false) && (appToken.Value > 0) && (!SubmittedApps.TryGetValue(appToken.Key, out ulong token) || (appToken.Value != token))).ToDictionary(static appToken => appToken.Key, static appToken => appToken.Value);
internal Dictionary<uint, string> GetDepotKeysForSubmission() => DepotKeys.Where(depotKey => (SteamTokenDumperPlugin.Config?.SecretDepotIDs.Contains(depotKey.Key) == false) && !string.IsNullOrEmpty(depotKey.Value) && (!SubmittedDepots.TryGetValue(depotKey.Key, out string? key) || (depotKey.Value != key))).ToDictionary(static depotKey => depotKey.Key, static depotKey => depotKey.Value);
internal Dictionary<uint, ulong> GetPackageTokensForSubmission() => PackageTokens.Where(packageToken => (SteamTokenDumperPlugin.Config?.SecretPackageIDs.Contains(packageToken.Key) == false) && (packageToken.Value > 0) && (!SubmittedPackages.TryGetValue(packageToken.Key, out ulong token) || (packageToken.Value != token))).ToDictionary(static packageToken => packageToken.Key, static packageToken => packageToken.Value);
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);
internal Dictionary<uint, string> GetDepotKeysForSubmission() => DepotKeys.Where(depotKey => (SteamTokenDumperPlugin.Config?.SecretDepotIDs.Contains(depotKey.Key) != true) && !string.IsNullOrEmpty(depotKey.Value) && (!SubmittedDepots.TryGetValue(depotKey.Key, out string? key) || (depotKey.Value != key))).ToDictionary(static depotKey => depotKey.Key, static depotKey => depotKey.Value);
internal Dictionary<uint, ulong> GetPackageTokensForSubmission() {
if (ASF.GlobalDatabase == null) {
throw new InvalidOperationException(nameof(ASF.GlobalDatabase));
}
return ASF.GlobalDatabase.PackageAccessTokensReadOnly.Where(packageToken => (SteamTokenDumperPlugin.Config?.SecretPackageIDs.Contains(packageToken.Key) != true) && (packageToken.Value > 0) && (!SubmittedPackages.TryGetValue(packageToken.Key, out ulong token) || (packageToken.Value != token))).ToDictionary(static packageToken => packageToken.Key, static packageToken => packageToken.Value);
}
internal static async Task<GlobalCache?> Load() {
if (!File.Exists(SharedFilePath)) {
@@ -117,7 +128,7 @@ internal sealed class GlobalCache : SerializableFile {
return null;
}
globalCache = JsonConvert.DeserializeObject<GlobalCache>(json);
globalCache = json.ToJsonObject<GlobalCache>();
} catch (Exception e) {
ASF.ArchiLogger.LogGenericException(e);
@@ -180,7 +191,6 @@ internal sealed class GlobalCache : SerializableFile {
if (clear) {
AppTokens.Clear();
DepotKeys.Clear();
PackageTokens.Clear();
}
Utilities.InBackground(Save);
@@ -261,25 +271,6 @@ internal sealed class GlobalCache : SerializableFile {
Utilities.InBackground(Save);
}
internal void UpdatePackageTokens(IReadOnlyCollection<KeyValuePair<uint, ulong>> packageTokens) {
ArgumentNullException.ThrowIfNull(packageTokens);
bool save = false;
foreach ((uint packageID, ulong packageToken) in packageTokens) {
if (PackageTokens.TryGetValue(packageID, out ulong previousPackageToken) && (previousPackageToken == packageToken)) {
continue;
}
PackageTokens[packageID] = packageToken;
save = true;
}
if (save) {
Utilities.InBackground(Save);
}
}
internal void UpdateSubmittedData(IReadOnlyDictionary<uint, ulong> apps, IReadOnlyDictionary<uint, ulong> packages, IReadOnlyDictionary<uint, string> depots) {
ArgumentNullException.ThrowIfNull(apps);
ArgumentNullException.ThrowIfNull(packages);
@@ -325,7 +316,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));
}
@@ -362,7 +353,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

@@ -4,7 +4,7 @@
// / ___ \ | | | (__ | | | || | ___) || |_| __/| (_| || | | | | || _|| (_| || | | | | | | |
// /_/ \_\|_| \___||_| |_||_||____/ \__|\___| \__,_||_| |_| |_||_| \__,_||_| |_| |_| |_|
// |
// 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 +19,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

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

@@ -4,7 +4,7 @@
// / ___ \ | | | (__ | | | || | ___) || |_| __/| (_| || | | | | || _|| (_| || | | | | | | |
// /_/ \_\|_| \___||_| |_||_||____/ \__|\___| \__,_||_| |_| |_||_| \__,_||_| |_| |_| |_|
// |
// 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

@@ -4,7 +4,7 @@
// / ___ \ | | | (__ | | | || | ___) || |_| __/| (_| || | | | | || _|| (_| || | | | | | | |
// /_/ \_\|_| \___||_| |_||_||____/ \__|\___| \__,_||_| |_| |_||_| \__,_||_| |_| |_| |_|
// |
// 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 +20,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; } = ImmutableHashSet<uint>.Empty;
[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; } = ImmutableHashSet<uint>.Empty;
[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; } = ImmutableHashSet<uint>.Empty;
[JsonProperty(Required = Required.DisallowNull)]
public bool SkipAutoGrantPackages { get; private set; } = true;
[JsonInclude]
public bool SkipAutoGrantPackages { get; private init; } = true;
[JsonConstructor]
internal SteamTokenDumperConfig() { }

View File

@@ -4,7 +4,7 @@
// / ___ \ | | | (__ | | | || | ___) || |_| __/| (_| || | | | | || _|| (_| || | | | | | | |
// /_/ \_\|_| \___||_| |_||_||____/ \__|\___| \__,_||_| |_| |_||_| \__,_||_| |_| |_| |_|
// |
// 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 +30,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

@@ -4,7 +4,7 @@
// / ___ \ | | | (__ | | | || | ___) || |_| __/| (_| || | | | | || _|| (_| || | | | | | | |
// /_/ \_\|_| \___||_| |_||_||____/ \__|\___| \__,_||_| |_| |_||_| \__,_||_| |_| |_| |_|
// |
// 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,27 +21,30 @@
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;
using ArchiSteamFarm.Plugins.Interfaces;
using ArchiSteamFarm.Steam;
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;
@@ -50,7 +53,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();
@@ -61,15 +63,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(Config?.Enabled == true ? GlobalCache?.LastChangeNumber ?? 0 : 0);
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)));
@@ -80,15 +84,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;
}
@@ -101,30 +109,6 @@ internal sealed class SteamTokenDumperPlugin : OfficialPlugin, IASF, IBot, IBotC
}
}
config ??= new SteamTokenDumperConfig();
if (isEnabled) {
config.Enabled = true;
}
if (!config.Enabled) {
ASF.ArchiLogger.LogGenericInfo(string.Format(CultureInfo.CurrentCulture, Strings.PluginDisabledInConfig, nameof(SteamTokenDumperPlugin)));
return;
}
if (!config.SecretAppIDs.IsEmpty) {
ASF.ArchiLogger.LogGenericInfo(string.Format(CultureInfo.CurrentCulture, Strings.PluginSecretListInitialized, nameof(config.SecretAppIDs), string.Join(", ", config.SecretAppIDs)));
}
if (!config.SecretPackageIDs.IsEmpty) {
ASF.ArchiLogger.LogGenericInfo(string.Format(CultureInfo.CurrentCulture, Strings.PluginSecretListInitialized, nameof(config.SecretPackageIDs), string.Join(", ", config.SecretPackageIDs)));
}
if (!config.SecretDepotIDs.IsEmpty) {
ASF.ArchiLogger.LogGenericInfo(string.Format(CultureInfo.CurrentCulture, Strings.PluginSecretListInitialized, nameof(config.SecretDepotIDs), string.Join(", ", config.SecretDepotIDs)));
}
if (GlobalCache == null) {
GlobalCache? globalCache = await GlobalCache.Load().ConfigureAwait(false);
@@ -137,8 +121,40 @@ internal sealed class SteamTokenDumperPlugin : OfficialPlugin, IASF, IBot, IBotC
}
}
if (!isEnabled && (config == null)) {
ASF.ArchiLogger.LogGenericInfo(string.Format(CultureInfo.CurrentCulture, Strings.PluginDisabledInConfig, nameof(SteamTokenDumperPlugin)));
return;
}
config ??= new SteamTokenDumperConfig();
if (isEnabled) {
config.Enabled = true;
}
if (!config.Enabled) {
ASF.ArchiLogger.LogGenericInfo(string.Format(CultureInfo.CurrentCulture, Strings.PluginDisabledInConfig, nameof(SteamTokenDumperPlugin)));
}
if (!config.SecretAppIDs.IsEmpty) {
ASF.ArchiLogger.LogGenericInfo(string.Format(CultureInfo.CurrentCulture, Strings.PluginSecretListInitialized, nameof(config.SecretAppIDs), string.Join(", ", config.SecretAppIDs)));
}
if (!config.SecretPackageIDs.IsEmpty) {
ASF.ArchiLogger.LogGenericInfo(string.Format(CultureInfo.CurrentCulture, Strings.PluginSecretListInitialized, nameof(config.SecretPackageIDs), string.Join(", ", config.SecretPackageIDs)));
}
if (!config.SecretDepotIDs.IsEmpty) {
ASF.ArchiLogger.LogGenericInfo(string.Format(CultureInfo.CurrentCulture, Strings.PluginSecretListInitialized, nameof(config.SecretDepotIDs), string.Join(", ", config.SecretDepotIDs)));
}
Config = config;
if (!config.Enabled) {
return;
}
#pragma warning disable CA5394 // This call isn't used in a security-sensitive manner
TimeSpan startIn = TimeSpan.FromMinutes(Random.Shared.Next(SharedInfo.MinimumMinutesBeforeFirstUpload, SharedInfo.MaximumMinutesBeforeFirstUpload));
#pragma warning restore CA5394 // This call isn't used in a security-sensitive manner
@@ -162,29 +178,24 @@ internal sealed class SteamTokenDumperPlugin : OfficialPlugin, IASF, IBot, IBotC
throw new ArgumentNullException(nameof(args));
}
switch (args.Length) {
case 1:
switch (args[0].ToUpperInvariant()) {
case "STD" when access >= EAccess.Owner:
if (Config is not { Enabled: true }) {
return Task.FromResult((string?) string.Format(CultureInfo.CurrentCulture, ArchiSteamFarm.Localization.Strings.WarningFailedWithError, nameof(Config)));
case "STD":
return Task.FromResult(ResponseRefreshManually(access, bot));
}
TimeSpan minimumTimeBetweenUpload = TimeSpan.FromMinutes(SharedInfo.MinimumMinutesBetweenUploads);
if (LastUploadAt + minimumTimeBetweenUpload > DateTimeOffset.UtcNow) {
return Task.FromResult((string?) string.Format(CultureInfo.CurrentCulture, Strings.SubmissionFailedTooManyRequests, minimumTimeBetweenUpload.ToHumanReadable()));
}
// ReSharper disable once SuspiciousLockOverSynchronizationPrimitive - this is not a mistake, we need extra synchronization, and we can re-use the semaphore object for that
lock (SubmissionSemaphore) {
SubmissionTimer.Change(TimeSpan.Zero, TimeSpan.FromHours(SharedInfo.HoursBetweenUploads));
}
return Task.FromResult((string?) ArchiSteamFarm.Localization.Strings.Done);
case "STD" when access > EAccess.None:
return Task.FromResult((string?) ArchiSteamFarm.Localization.Strings.ErrorAccessDenied);
break;
default:
return Task.FromResult((string?) null);
switch (args[0].ToUpperInvariant()) {
case "STD":
return Task.FromResult(ResponseRefreshManually(access, Utilities.GetArgsAsText(args, 1, ","), steamID));
}
break;
}
return Task.FromResult((string?) null);
}
public async Task OnBotDestroy(Bot bot) {
@@ -207,7 +218,8 @@ internal sealed class SteamTokenDumperPlugin : OfficialPlugin, IASF, IBot, IBotC
public async Task OnBotInit(Bot bot) {
ArgumentNullException.ThrowIfNull(bot);
if (Config is not { Enabled: true }) {
if (GlobalCache == null) {
// We can't operate like this anyway, skip initialization of synchronization structures
return;
}
@@ -255,15 +267,7 @@ internal sealed class SteamTokenDumperPlugin : OfficialPlugin, IASF, IBot, IBotC
ArgumentNullException.ThrowIfNull(appChanges);
ArgumentNullException.ThrowIfNull(packageChanges);
if (Config is not { Enabled: true }) {
return Task.CompletedTask;
}
if (GlobalCache == null) {
throw new InvalidOperationException(nameof(GlobalCache));
}
GlobalCache.OnPICSChanges(currentChangeNumber, appChanges);
GlobalCache?.OnPICSChanges(currentChangeNumber, appChanges);
return Task.CompletedTask;
}
@@ -271,15 +275,7 @@ internal sealed class SteamTokenDumperPlugin : OfficialPlugin, IASF, IBot, IBotC
public Task OnPICSChangesRestart(uint currentChangeNumber) {
ArgumentOutOfRangeException.ThrowIfZero(currentChangeNumber);
if (Config is not { Enabled: true }) {
return Task.CompletedTask;
}
if (GlobalCache == null) {
throw new InvalidOperationException(nameof(GlobalCache));
}
GlobalCache.OnPICSChangesRestart(currentChangeNumber);
GlobalCache?.OnPICSChangesRestart(currentChangeNumber);
return Task.CompletedTask;
}
@@ -300,32 +296,34 @@ internal sealed class SteamTokenDumperPlugin : OfficialPlugin, IASF, IBot, IBotC
return;
}
if (GlobalCache == null) {
throw new InvalidOperationException(nameof(GlobalCache));
// Schedule a refresh in a while from now
if (!BotSynchronizations.TryGetValue(bot, out (SemaphoreSlim RefreshSemaphore, Timer RefreshTimer) synchronization)) {
return;
}
Dictionary<uint, ulong> packageTokens = callback.LicenseList.Where(static license => !Config.SecretPackageIDs.Contains(license.PackageID) && ((license.PaymentMethod != EPaymentMethod.AutoGrant) || !Config.SkipAutoGrantPackages)).GroupBy(static license => license.PackageID).ToDictionary(static group => group.Key, static group => group.OrderByDescending(static license => license.TimeCreated).First().AccessToken);
if (!await synchronization.RefreshSemaphore.WaitAsync(0).ConfigureAwait(false)) {
// Another refresh is in progress, skip the refresh for now
return;
}
GlobalCache.UpdatePackageTokens(packageTokens);
await Refresh(bot, packageTokens.Keys).ConfigureAwait(false);
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 (Config is not { Enabled: true }) {
return;
}
if (GlobalCache == null) {
throw new InvalidOperationException(nameof(GlobalCache));
}
if (ASF.GlobalDatabase == null) {
throw new InvalidOperationException(nameof(GlobalCache));
throw new InvalidOperationException(nameof(ASF.GlobalDatabase));
}
if (!BotSynchronizations.TryGetValue(bot, out (SemaphoreSlim RefreshSemaphore, Timer RefreshTimer) synchronization)) {
@@ -343,17 +341,17 @@ internal sealed class SteamTokenDumperPlugin : OfficialPlugin, IASF, IBot, IBotC
return;
}
packageIDs ??= bot.OwnedPackageIDs.Where(static package => !Config.SecretPackageIDs.Contains(package.Key) && ((package.Value.PaymentMethod != EPaymentMethod.AutoGrant) || !Config.SkipAutoGrantPackages)).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))) {
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)) {
// ASF might not have the package info for us at the moment, we'll retry later
continue;
}
appIDsToRefresh.UnionWith(packageData.AppIDs.Where(static appID => !Config.SecretAppIDs.Contains(appID) && GlobalCache.ShouldRefreshAppInfo(appID)));
appIDsToRefresh.UnionWith(packageData.AppIDs.Where(static appID => (Config?.SecretAppIDs.Contains(appID) != true) && GlobalCache.ShouldRefreshAppInfo(appID)));
}
if (appIDsToRefresh.Count == 0) {
@@ -405,7 +403,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) {
@@ -458,7 +456,7 @@ internal sealed class SteamTokenDumperPlugin : OfficialPlugin, IASF, IBot, IBotC
bool shouldFetchMainKey = false;
foreach (KeyValue depot in app.KeyValues["depots"].Children) {
if (!uint.TryParse(depot.Name, out uint depotID) || (knownDepotIDs?.Contains(depotID) == true) || Config.SecretDepotIDs.Contains(depotID) || !GlobalCache.ShouldRefreshDepotKey(depotID)) {
if (!uint.TryParse(depot.Name, out uint depotID) || (knownDepotIDs?.Contains(depotID) == true) || (Config?.SecretDepotIDs.Contains(depotID) == true) || !GlobalCache.ShouldRefreshDepotKey(depotID)) {
continue;
}
@@ -521,7 +519,9 @@ internal sealed class SteamTokenDumperPlugin : OfficialPlugin, IASF, IBot, IBotC
}
}
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
@@ -534,9 +534,11 @@ internal sealed class SteamTokenDumperPlugin : OfficialPlugin, IASF, IBot, IBotC
bot.ArchiLogger.LogGenericInfo(string.Format(CultureInfo.CurrentCulture, Strings.BotFinishedRetrievingTotalDepots, appIDsToRefresh.Count));
} finally {
if (Config?.Enabled == true) {
TimeSpan timeSpan = TimeSpan.FromHours(SharedInfo.MaximumHoursBetweenRefresh);
synchronization.RefreshTimer.Change(timeSpan, timeSpan);
}
await depotsRateLimitingSemaphore.WaitAsync().ConfigureAwait(false);
@@ -546,15 +548,74 @@ internal sealed class SteamTokenDumperPlugin : OfficialPlugin, IASF, IBot, IBotC
}
}
private static string? ResponseRefreshManually(EAccess access, Bot bot) {
if (!Enum.IsDefined(access)) {
throw new InvalidEnumArgumentException(nameof(access), (int) access, typeof(EAccess));
}
ArgumentNullException.ThrowIfNull(bot);
if (access < EAccess.Master) {
return access > EAccess.None ? bot.Commands.FormatBotResponse(ArchiSteamFarm.Localization.Strings.ErrorAccessDenied) : null;
}
if (GlobalCache == null) {
return bot.Commands.FormatBotResponse(string.Format(CultureInfo.CurrentCulture, ArchiSteamFarm.Localization.Strings.WarningFailedWithError, nameof(GlobalCache)));
}
Utilities.InBackground(
async () => {
await Refresh(bot).ConfigureAwait(false);
await SubmitData().ConfigureAwait(false);
}
);
return bot.Commands.FormatBotResponse(ArchiSteamFarm.Localization.Strings.Done);
}
private static string? ResponseRefreshManually(EAccess access, string botNames, ulong steamID = 0) {
if (!Enum.IsDefined(access)) {
throw new InvalidEnumArgumentException(nameof(access), (int) access, typeof(EAccess));
}
ArgumentException.ThrowIfNullOrEmpty(botNames);
if ((steamID != 0) && !new SteamID(steamID).IsIndividualAccount) {
throw new ArgumentOutOfRangeException(nameof(steamID));
}
HashSet<Bot>? bots = Bot.GetBots(botNames);
if ((bots == null) || (bots.Count == 0)) {
return access >= EAccess.Owner ? 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)));
}
Utilities.InBackground(
async () => {
await Utilities.InParallel(bots.Select(static bot => Refresh(bot))).ConfigureAwait(false);
await SubmitData().ConfigureAwait(false);
}
);
return Commands.FormatStaticResponse(ArchiSteamFarm.Localization.Strings.Done);
}
private static async Task SubmitData(CancellationToken cancellationToken = default) {
if (Bot.Bots == null) {
throw new InvalidOperationException(nameof(Bot.Bots));
}
if (Config is not { Enabled: true }) {
return;
}
if (GlobalCache == null) {
throw new InvalidOperationException(nameof(GlobalCache));
}
@@ -610,7 +671,7 @@ internal sealed class SteamTokenDumperPlugin : OfficialPlugin, IASF, IBot, IBotC
ASF.ArchiLogger.LogGenericWarning(string.Format(CultureInfo.CurrentCulture, ArchiSteamFarm.Localization.Strings.WarningFailedWithError, response.StatusCode));
switch (response.StatusCode) {
case HttpStatusCode.Forbidden:
case HttpStatusCode.Forbidden when Config?.Enabled == true:
// SteamDB told us to stop submitting data for now
// ReSharper disable once SuspiciousLockOverSynchronizationPrimitive - this is not a mistake, we need extra synchronization, and we can re-use the semaphore object for that
lock (SubmissionSemaphore) {
@@ -623,7 +684,7 @@ internal sealed class SteamTokenDumperPlugin : OfficialPlugin, IASF, IBot, IBotC
GlobalCache.Reset(true);
break;
case HttpStatusCode.TooManyRequests:
case HttpStatusCode.TooManyRequests when Config?.Enabled == true:
// SteamDB told us to try again later
#pragma warning disable CA5394 // This call isn't used in a security-sensitive manner
TimeSpan startIn = TimeSpan.FromMinutes(Random.Shared.Next(SharedInfo.MinimumMinutesBeforeFirstUpload, SharedInfo.MaximumMinutesBeforeFirstUpload));
@@ -649,7 +710,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

@@ -4,7 +4,7 @@
// / ___ \ | | | (__ | | | || | ___) || |_| __/| (_| || | | | | || _|| (_| || | | | | | | |
// /_/ \_\|_| \___||_| |_||_||____/ \__|\___| \__,_||_| |_| |_||_| \__,_||_| |_| |_| |_|
// |
// 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

@@ -4,7 +4,7 @@
// / ___ \ | | | (__ | | | || | ___) || |_| __/| (_| || | | | | || _|| (_| || | | | | | | |
// /_/ \_\|_| \___||_| |_||_||____/ \__|\___| \__,_||_| |_| |_||_| \__,_||_| |_| |_| |_|
// |
// 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,7 +39,7 @@ 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++) {
@@ -55,30 +55,27 @@ public sealed class Bot {
}
[TestMethod]
[ExpectedException(typeof(ArgumentOutOfRangeException))]
public void MaxItemsTooSmall() {
const uint appID = 42;
HashSet<Asset> items = new() {
HashSet<Asset> items = [
CreateCard(1, appID),
CreateCard(2, 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() {
HashSet<Asset> items = [
CreateCard(1, appID),
CreateCard(1, appID),
CreateCard(2, appID),
CreateCard(3, appID)
};
];
HashSet<Asset> itemsToSend = GetItemsForFullBadge(items, 3, appID);
@@ -95,12 +92,12 @@ public sealed class Bot {
public void MultipleSets() {
const uint appID = 42;
HashSet<Asset> items = new() {
HashSet<Asset> items = [
CreateCard(1, appID),
CreateCard(1, appID),
CreateCard(2, appID),
CreateCard(2, appID)
};
];
HashSet<Asset> itemsToSend = GetItemsForFullBadge(items, 2, appID);
@@ -116,11 +113,11 @@ public sealed class Bot {
public void MultipleSetsDifferentAmount() {
const uint appID = 42;
HashSet<Asset> items = new() {
HashSet<Asset> items = [
CreateCard(1, appID, 2),
CreateCard(2, appID),
CreateCard(2, appID)
};
];
HashSet<Asset> itemsToSend = GetItemsForFullBadge(items, 2, appID);
@@ -136,7 +133,7 @@ public sealed class Bot {
public void MutliRarityAndType() {
const uint appID = 42;
HashSet<Asset> items = new() {
HashSet<Asset> items = [
CreateCard(1, appID, type: Asset.EType.TradingCard, rarity: Asset.ERarity.Common),
CreateCard(2, appID, type: Asset.EType.TradingCard, rarity: Asset.ERarity.Common),
@@ -158,7 +155,7 @@ public sealed class Bot {
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)
};
];
HashSet<Asset> itemsToSend = GetItemsForFullBadge(items, 3, appID);
@@ -177,10 +174,10 @@ public sealed class Bot {
public void NotAllCardsPresent() {
const uint appID = 42;
HashSet<Asset> items = new() {
HashSet<Asset> items = [
CreateCard(1, appID),
CreateCard(2, appID)
};
];
HashSet<Asset> itemsToSend = GetItemsForFullBadge(items, 3, appID);
@@ -192,10 +189,10 @@ public sealed class Bot {
public void OneSet() {
const uint appID = 42;
HashSet<Asset> items = new() {
HashSet<Asset> items = [
CreateCard(1, appID),
CreateCard(2, appID)
};
];
HashSet<Asset> itemsToSend = GetItemsForFullBadge(items, 2, appID);
@@ -212,10 +209,10 @@ public sealed class Bot {
const uint appID0 = 42;
const uint appID1 = 43;
HashSet<Asset> items = new() {
HashSet<Asset> items = [
CreateCard(1, appID0),
CreateCard(1, appID1)
};
];
HashSet<Asset> itemsToSend = GetItemsForFullBadge(
items, new Dictionary<uint, byte> {
@@ -237,10 +234,10 @@ public sealed class Bot {
const uint appID0 = 42;
const uint appID1 = 43;
HashSet<Asset> items = new() {
HashSet<Asset> items = [
CreateCard(1, appID0),
CreateCard(1, appID1)
};
];
HashSet<Asset> itemsToSend = GetItemsForFullBadge(
items, new Dictionary<uint, byte> {
@@ -260,14 +257,14 @@ public sealed class Bot {
const uint appID1 = 43;
const uint appID2 = 44;
HashSet<Asset> items = new() {
HashSet<Asset> items = [
CreateCard(1, appID0),
CreateCard(2, appID0),
CreateCard(1, appID1),
CreateCard(2, appID1),
CreateCard(3, appID1)
};
];
HashSet<Asset> itemsToSend = GetItemsForFullBadge(
items, new Dictionary<uint, byte> {
@@ -290,10 +287,10 @@ public sealed class Bot {
public void OtherRarityFullSets() {
const uint appID = 42;
HashSet<Asset> items = new() {
HashSet<Asset> items = [
CreateCard(1, appID, rarity: Asset.ERarity.Common),
CreateCard(1, appID, rarity: Asset.ERarity.Rare)
};
];
HashSet<Asset> itemsToSend = GetItemsForFullBadge(items, 1, appID);
@@ -308,10 +305,10 @@ public sealed class Bot {
public void OtherRarityNoSets() {
const uint appID = 42;
HashSet<Asset> items = new() {
HashSet<Asset> items = [
CreateCard(1, appID, rarity: Asset.ERarity.Common),
CreateCard(1, appID, rarity: Asset.ERarity.Rare)
};
];
HashSet<Asset> itemsToSend = GetItemsForFullBadge(items, 2, appID);
@@ -324,13 +321,13 @@ public sealed class Bot {
public void OtherRarityOneSet() {
const uint appID = 42;
HashSet<Asset> items = new() {
HashSet<Asset> items = [
CreateCard(1, appID, rarity: Asset.ERarity.Common),
CreateCard(2, appID, rarity: Asset.ERarity.Common),
CreateCard(1, appID, rarity: Asset.ERarity.Uncommon),
CreateCard(2, appID, rarity: Asset.ERarity.Uncommon),
CreateCard(3, appID, rarity: Asset.ERarity.Uncommon)
};
];
HashSet<Asset> itemsToSend = GetItemsForFullBadge(items, 3, appID);
@@ -347,10 +344,10 @@ public sealed class Bot {
public void OtherTypeFullSets() {
const uint appID = 42;
HashSet<Asset> items = new() {
HashSet<Asset> items = [
CreateCard(1, appID, type: Asset.EType.TradingCard),
CreateCard(1, appID, type: Asset.EType.FoilTradingCard)
};
];
HashSet<Asset> itemsToSend = GetItemsForFullBadge(items, 1, appID);
@@ -365,10 +362,10 @@ public sealed class Bot {
public void OtherTypeNoSets() {
const uint appID = 42;
HashSet<Asset> items = new() {
HashSet<Asset> items = [
CreateCard(1, appID, type: Asset.EType.TradingCard),
CreateCard(1, appID, type: Asset.EType.FoilTradingCard)
};
];
HashSet<Asset> itemsToSend = GetItemsForFullBadge(items, 2, appID);
@@ -381,13 +378,13 @@ public sealed class Bot {
public void OtherTypeOneSet() {
const uint appID = 42;
HashSet<Asset> items = new() {
HashSet<Asset> items = [
CreateCard(1, appID, type: Asset.EType.TradingCard),
CreateCard(2, appID, type: Asset.EType.TradingCard),
CreateCard(1, appID, type: Asset.EType.FoilTradingCard),
CreateCard(2, appID, type: Asset.EType.FoilTradingCard),
CreateCard(3, appID, type: Asset.EType.FoilTradingCard)
};
];
HashSet<Asset> itemsToSend = GetItemsForFullBadge(items, 3, appID);
@@ -404,10 +401,10 @@ public sealed class Bot {
public void TooHighAmount() {
const uint appID0 = 42;
HashSet<Asset> items = new() {
HashSet<Asset> items = [
CreateCard(1, appID0, 2),
CreateCard(2, appID0)
};
];
HashSet<Asset> itemsToSend = GetItemsForFullBadge(items, 2, appID0);
@@ -423,7 +420,7 @@ 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));
@@ -440,7 +437,7 @@ 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));
@@ -460,28 +457,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() {
HashSet<Asset> items = [
CreateCard(1, appID0),
CreateCard(2, appID0),
CreateCard(3, appID0),
CreateCard(4, appID0)
};
];
GetItemsForFullBadge(
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) {

View File

@@ -4,7 +4,7 @@
// / ___ \ | | | (__ | | | || | ___) || |_| __/| (_| || | | | | || _|| (_| || | | | | | | |
// /_/ \_\|_| \___||_| |_||_||____/ \__|\___| \__,_||_| |_| |_||_| \__,_||_| |_| |_| |_|
// |
// 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,7 +200,8 @@ public sealed class SteamChatMessage {
public async Task RyzhehvostInitialTestForSplitting() {
const string prefix = "/me ";
const string message = @"<XLimited5> Уже имеет: app/1493800 | Aircraft Carrier Survival: Prolouge
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
@@ -253,7 +254,8 @@ public sealed class SteamChatMessage {
<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/304930 | Unturned.
""";
List<string> output = await GetMessageParts(message, prefix).ToListAsync().ConfigureAwait(false);
@@ -309,27 +311,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

@@ -4,7 +4,7 @@
// / ___ \ | | | (__ | | | || | ___) || |_| __/| (_| || | | | | || _|| (_| || | | | | | | |
// /_/ \_\|_| \___||_| |_||_||____/ \__|\___| \__,_||_| |_| |_||_| \__,_||_| |_| |_| |_|
// |
// 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 +30,21 @@ namespace ArchiSteamFarm.Tests;
public sealed class Trading {
[TestMethod]
public void ExploitingNewSetsIsFairButNotNeutral() {
HashSet<Asset> inventory = new() {
HashSet<Asset> inventory = [
CreateItem(1, 40),
CreateItem(2, 10),
CreateItem(3, 10)
};
];
HashSet<Asset> itemsToGive = new() {
HashSet<Asset> itemsToGive = [
CreateItem(2, 5),
CreateItem(3, 5)
};
];
HashSet<Asset> itemsToReceive = new() {
HashSet<Asset> itemsToReceive = [
CreateItem(1, 9),
CreateItem(4)
};
];
Assert.IsTrue(IsFairExchange(itemsToGive, itemsToReceive));
Assert.IsFalse(IsTradeNeutralOrBetter(inventory, itemsToGive, itemsToReceive));
@@ -52,45 +52,45 @@ 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: Asset.ERarity.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: Asset.EType.Emoticon)];
HashSet<Asset> itemsToReceive = [CreateItem(2)];
Assert.IsFalse(IsFairExchange(itemsToGive, itemsToReceive));
}
[TestMethod]
public void MultiGameMultiTypeBadReject() {
HashSet<Asset> inventory = new() {
HashSet<Asset> inventory = [
CreateItem(1, 9),
CreateItem(3, 9, 730, Asset.EType.Emoticon),
CreateItem(4, realAppID: 730, type: Asset.EType.Emoticon)
};
];
HashSet<Asset> itemsToGive = new() {
HashSet<Asset> itemsToGive = [
CreateItem(1),
CreateItem(4, realAppID: 730, type: Asset.EType.Emoticon)
};
];
HashSet<Asset> itemsToReceive = new() {
HashSet<Asset> itemsToReceive = [
CreateItem(2),
CreateItem(3, realAppID: 730, type: Asset.EType.Emoticon)
};
];
Assert.IsTrue(IsFairExchange(itemsToGive, itemsToReceive));
Assert.IsFalse(IsTradeNeutralOrBetter(inventory, itemsToGive, itemsToReceive));
@@ -98,20 +98,20 @@ public sealed class Trading {
[TestMethod]
public void MultiGameMultiTypeNeutralAccept() {
HashSet<Asset> inventory = new() {
HashSet<Asset> inventory = [
CreateItem(1, 9),
CreateItem(3, realAppID: 730, type: Asset.EType.Emoticon)
};
];
HashSet<Asset> itemsToGive = new() {
HashSet<Asset> itemsToGive = [
CreateItem(1),
CreateItem(3, realAppID: 730, type: Asset.EType.Emoticon)
};
];
HashSet<Asset> itemsToReceive = new() {
HashSet<Asset> itemsToReceive = [
CreateItem(2),
CreateItem(4, realAppID: 730, type: Asset.EType.Emoticon)
};
];
Assert.IsTrue(IsFairExchange(itemsToGive, itemsToReceive));
Assert.IsTrue(IsTradeNeutralOrBetter(inventory, itemsToGive, itemsToReceive));
@@ -119,21 +119,21 @@ public sealed class Trading {
[TestMethod]
public void MultiGameSingleTypeBadReject() {
HashSet<Asset> inventory = new() {
HashSet<Asset> inventory = [
CreateItem(1, 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 +141,20 @@ public sealed class Trading {
[TestMethod]
public void MultiGameSingleTypeNeutralAccept() {
HashSet<Asset> inventory = new() {
HashSet<Asset> inventory = [
CreateItem(1, 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 +162,17 @@ public sealed class Trading {
[TestMethod]
public void SingleGameAbrynosWasWrongNeutralAccept() {
HashSet<Asset> inventory = new() {
HashSet<Asset> inventory = [
CreateItem(1),
CreateItem(2, 2),
CreateItem(3),
CreateItem(4),
CreateItem(5)
};
];
HashSet<Asset> itemsToGive = new() {
CreateItem(2)
};
HashSet<Asset> itemsToGive = [CreateItem(2)];
HashSet<Asset> itemsToReceive = new() {
CreateItem(3)
};
HashSet<Asset> itemsToReceive = [CreateItem(3)];
Assert.IsTrue(IsFairExchange(itemsToGive, itemsToReceive));
Assert.IsTrue(IsTradeNeutralOrBetter(inventory, itemsToGive, itemsToReceive));
@@ -184,18 +180,14 @@ public sealed class Trading {
[TestMethod]
public void SingleGameDonationAccept() {
HashSet<Asset> inventory = new() {
CreateItem(1)
};
HashSet<Asset> inventory = [CreateItem(1)];
HashSet<Asset> itemsToGive = new() {
CreateItem(1)
};
HashSet<Asset> itemsToGive = [CreateItem(1)];
HashSet<Asset> itemsToReceive = new() {
HashSet<Asset> itemsToReceive = [
CreateItem(2),
CreateItem(3, type: Asset.EType.SteamGems)
};
];
Assert.IsTrue(IsFairExchange(itemsToGive, itemsToReceive));
Assert.IsTrue(IsTradeNeutralOrBetter(inventory, itemsToGive, itemsToReceive));
@@ -203,21 +195,21 @@ public sealed class Trading {
[TestMethod]
public void SingleGameMultiTypeBadReject() {
HashSet<Asset> inventory = new() {
HashSet<Asset> inventory = [
CreateItem(1, 9),
CreateItem(3, 9, type: Asset.EType.Emoticon),
CreateItem(4, type: Asset.EType.Emoticon)
};
];
HashSet<Asset> itemsToGive = new() {
HashSet<Asset> itemsToGive = [
CreateItem(1),
CreateItem(4, type: Asset.EType.Emoticon)
};
];
HashSet<Asset> itemsToReceive = new() {
HashSet<Asset> itemsToReceive = [
CreateItem(2),
CreateItem(3, type: Asset.EType.Emoticon)
};
];
Assert.IsTrue(IsFairExchange(itemsToGive, itemsToReceive));
Assert.IsFalse(IsTradeNeutralOrBetter(inventory, itemsToGive, itemsToReceive));
@@ -225,20 +217,20 @@ public sealed class Trading {
[TestMethod]
public void SingleGameMultiTypeNeutralAccept() {
HashSet<Asset> inventory = new() {
HashSet<Asset> inventory = [
CreateItem(1, 9),
CreateItem(3, type: Asset.EType.Emoticon)
};
];
HashSet<Asset> itemsToGive = new() {
HashSet<Asset> itemsToGive = [
CreateItem(1),
CreateItem(3, type: Asset.EType.Emoticon)
};
];
HashSet<Asset> itemsToReceive = new() {
HashSet<Asset> itemsToReceive = [
CreateItem(2),
CreateItem(4, type: Asset.EType.Emoticon)
};
];
Assert.IsTrue(IsFairExchange(itemsToGive, itemsToReceive));
Assert.IsTrue(IsTradeNeutralOrBetter(inventory, itemsToGive, itemsToReceive));
@@ -246,19 +238,19 @@ 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, 3)];
Assert.IsTrue(IsFairExchange(itemsToGive, itemsToReceive));
Assert.IsFalse(IsTradeNeutralOrBetter(inventory, itemsToGive, itemsToReceive));
@@ -266,17 +258,17 @@ public sealed class Trading {
[TestMethod]
public void SingleGameQuantityBadReject2() {
HashSet<Asset> inventory = new() {
HashSet<Asset> inventory = [
CreateItem(1),
CreateItem(2, 2)
};
];
HashSet<Asset> itemsToGive = new() {
HashSet<Asset> itemsToGive = [
CreateItem(1),
CreateItem(2, 2)
};
];
HashSet<Asset> itemsToReceive = new() { CreateItem(3, 3) };
HashSet<Asset> itemsToReceive = [CreateItem(3, 3)];
Assert.IsTrue(IsFairExchange(itemsToGive, itemsToReceive));
Assert.IsFalse(IsTradeNeutralOrBetter(inventory, itemsToGive, itemsToReceive));
@@ -284,17 +276,17 @@ public sealed class Trading {
[TestMethod]
public void SingleGameQuantityNeutralAccept() {
HashSet<Asset> inventory = new() {
HashSet<Asset> inventory = [
CreateItem(1, 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, 2)];
Assert.IsTrue(IsFairExchange(itemsToGive, itemsToReceive));
Assert.IsTrue(IsTradeNeutralOrBetter(inventory, itemsToGive, itemsToReceive));
@@ -302,13 +294,13 @@ 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 +308,18 @@ public sealed class Trading {
[TestMethod]
public void SingleGameSingleTypeBadWithOverpayingReject() {
HashSet<Asset> inventory = new() {
HashSet<Asset> inventory = [
CreateItem(1, 2),
CreateItem(2, 2),
CreateItem(3, 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 +327,14 @@ public sealed class Trading {
[TestMethod]
public void SingleGameSingleTypeBigDifferenceAccept() {
HashSet<Asset> inventory = new() {
HashSet<Asset> inventory = [
CreateItem(1),
CreateItem(2, 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 +342,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)
};
];
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 +366,9 @@ 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, 2)];
HashSet<Asset> itemsToGive = [CreateItem(1)];
HashSet<Asset> itemsToReceive = [CreateItem(2)];
Assert.IsTrue(IsFairExchange(itemsToGive, itemsToReceive));
Assert.IsTrue(IsTradeNeutralOrBetter(inventory, itemsToGive, itemsToReceive));
@@ -384,9 +376,9 @@ 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 +386,17 @@ public sealed class Trading {
[TestMethod]
public void SingleGameSingleTypeNeutralWithOverpayingAccept() {
HashSet<Asset> inventory = new() {
HashSet<Asset> inventory = [
CreateItem(1, 2),
CreateItem(2, 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,23 +404,23 @@ public sealed class Trading {
[TestMethod]
public void TakingExcessiveAmountOfSingleCardCanStillBeFairAndNeutral() {
HashSet<Asset> inventory = new() {
HashSet<Asset> inventory = [
CreateItem(1, 52),
CreateItem(2, 73),
CreateItem(3, 52),
CreateItem(4, 47),
CreateItem(5)
};
];
HashSet<Asset> itemsToGive = new() { CreateItem(2, 73) };
HashSet<Asset> itemsToGive = [CreateItem(2, 73)];
HashSet<Asset> itemsToReceive = new() {
HashSet<Asset> itemsToReceive = [
CreateItem(1, 9),
CreateItem(3, 9),
CreateItem(4, 8),
CreateItem(5, 24),
CreateItem(6, 23)
};
];
Assert.IsTrue(IsFairExchange(itemsToGive, itemsToReceive));
Assert.IsTrue(IsTradeNeutralOrBetter(inventory, itemsToGive, itemsToReceive));

View File

@@ -4,7 +4,7 @@
// / ___ \ | | | (__ | | | || | ___) || |_| __/| (_| || | | | | || _|| (_| || | | | | | | |
// /_/ \_\|_| \___||_| |_||_||____/ \__|\___| \__,_||_| |_| |_||_| \__,_||_| |_| |_| |_|
// |
// 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

@@ -4,7 +4,7 @@
// / ___ \ | | | (__ | | | || | ___) || |_| __/| (_| || | | | | || _|| (_| || | | | | | | |
// /_/ \_\|_| \___||_| |_||_||____/ \__|\___| \__,_||_| |_| |_||_| \__,_||_| |_| |_| |_|
// |
// 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

@@ -4,7 +4,7 @@
// / ___ \ | | | (__ | | | || | ___) || |_| __/| (_| || | | | | || _|| (_| || | | | | | | |
// /_/ \_\|_| \___||_| |_||_||____/ \__|\___| \__,_||_| |_| |_||_| \__,_||_| |_| |_| |_|
// |
// 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

@@ -4,7 +4,7 @@
// / ___ \ | | | (__ | | | || | ___) || |_| __/| (_| || | | | | || _|| (_| || | | | | | | |
// /_/ \_\|_| \___||_| |_||_||____/ \__|\___| \__,_||_| |_| |_||_| \__,_||_| |_| |_| |_|
// |
// 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 +24,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 +38,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 +82,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 +106,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 +116,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 +168,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 +199,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 +222,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 +233,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 +255,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

@@ -4,7 +4,7 @@
// / ___ \ | | | (__ | | | || | ___) || |_| __/| (_| || | | | | || _|| (_| || | | | | | | |
// /_/ \_\|_| \___||_| |_||_||____/ \__|\___| \__,_||_| |_| |_||_| \__,_||_| |_| |_| |_|
// |
// 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 +22,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 +41,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 +57,8 @@ internal sealed class ConcurrentList<T> : IList<T>, IReadOnlyList<T> {
}
set {
ArgumentNullException.ThrowIfNull(value);
using (Lock.WriterLock()) {
BackingCollection[index] = value;
}
@@ -63,7 +67,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 +95,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 +114,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 +133,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 +147,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 +158,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

@@ -4,7 +4,7 @@
// / ___ \ | | | (__ | | | || | ___) || |_| __/| (_| || | | | | || _|| (_| || | | | | | | |
// /_/ \_\|_| \___||_| |_||_||____/ \__|\___| \__,_||_| |_| |_||_| \__,_||_| |_| |_| |_|
// |
// 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 +23,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 +53,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

@@ -4,7 +4,7 @@
// / ___ \ | | | (__ | | | || | ___) || |_| __/| (_| || | | | | || _|| (_| || | | | | | | |
// /_/ \_\|_| \___||_| |_||_||____/ \__|\___| \__,_||_| |_| |_||_| \__,_||_| |_| |_| |_|
// |
// 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 +23,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 +43,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 +54,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 +67,39 @@ 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);
TryAdd(key, value);
}
public void Clear() {
if (BackingDictionary.IsEmpty) {
@@ -82,7 +111,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 +134,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 +145,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 +185,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

@@ -4,7 +4,7 @@
// / ___ \ | | | (__ | | | || | ___) || |_| __/| (_| || | | | | || _|| (_| || | | | | | | |
// /_/ \_\|_| \___||_| |_||_||____/ \__|\___| \__,_||_| |_| |_||_| \__,_||_| |_| |_| |_|
// |
// 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 +21,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;
@@ -75,9 +75,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 +101,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);
await UpdateAndRestart().ConfigureAwait(false);
if (!Program.IgnoreUnsupportedEnvironment && !await ProtectAgainstCrashes().ConfigureAwait(false)) {
ArchiLogger.LogFatalError(Strings.ErrorTooManyCrashes);
return true;
}
Program.AllowCrashFileRemoval = true;
if (!await PluginsCore.InitPlugins().ConfigureAwait(false)) {
return false;
}
await PluginsCore.OnASFInitModules(GlobalConfig.AdditionalProperties).ConfigureAwait(false);
await InitRateLimiters().ConfigureAwait(false);
@@ -142,6 +151,8 @@ public static class ASF {
if (Program.ConfigWatch) {
InitConfigWatchEvents();
}
return true;
}
internal static bool IsValidBotName(string botName) {
@@ -441,7 +452,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() {
@@ -813,6 +824,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));
@@ -905,6 +947,9 @@ public static class ASF {
return;
}
// Allow crash file recovery, if needed
Program.AllowCrashFileRemoval = true;
await RestartOrExit().ConfigureAwait(false);
}
@@ -1047,6 +1092,7 @@ public static class ASF {
internal enum EFileType : byte {
Config,
Database
Database,
Crash
}
}

View File

@@ -4,7 +4,7 @@
// / ___ \ | | | (__ | | | || | ___) || |_| __/| (_| || | | | | || _|| (_| || | | | | | | |
// /_/ \_\|_| \___||_| |_||_||____/ \__|\___| \__,_||_| |_| |_||_| \__,_||_| |_| |_| |_|
// |
// 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

@@ -4,7 +4,7 @@
// / ___ \ | | | (__ | | | || | ___) || |_| __/| (_| || | | | | || _|| (_| || | | | | | | |
// /_/ \_\|_| \___||_| |_||_||____/ \__|\___| \__,_||_| |_| |_||_| \__,_||_| |_| |_| |_|
// |
// 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

@@ -4,7 +4,7 @@
// / ___ \ | | | (__ | | | || | ___) || |_| __/| (_| || | | | | || _|| (_| || | | | | | | |
// /_/ \_\|_| \___||_| |_||_||____/ \__|\___| \__,_||_| |_| |_||_| \__,_||_| |_| |_| |_|
// |
// 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

@@ -4,7 +4,7 @@
// / ___ \ | | | (__ | | | || | ___) || |_| __/| (_| || | | | | || _|| (_| || | | | | | | |
// /_/ \_\|_| \___||_| |_||_||____/ \__|\___| \__,_||_| |_| |_||_| \__,_||_| |_| |_| |_|
// |
// 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

@@ -4,7 +4,7 @@
// / ___ \ | | | (__ | | | || | ___) || |_| __/| (_| || | | | | || _|| (_| || | | | | | | |
// /_/ \_\|_| \___||_| |_||_||____/ \__|\___| \__,_||_| |_| |_||_| \__,_||_| |_| |_| |_|
// |
// 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

@@ -4,7 +4,7 @@
// / ___ \ | | | (__ | | | || | ___) || |_| __/| (_| || | | | | || _|| (_| || | | | | | | |
// /_/ \_\|_| \___||_| |_||_||____/ \__|\___| \__,_||_| |_| |_||_| \__,_||_| |_| |_| |_|
// |
// 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

@@ -4,7 +4,7 @@
// / ___ \ | | | (__ | | | || | ___) || |_| __/| (_| || | | | | || _|| (_| || | | | | | | |
// /_/ \_\|_| \___||_| |_||_||____/ \__|\___| \__,_||_| |_| |_||_| \__,_||_| |_| |_| |_|
// |
// 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

@@ -4,7 +4,7 @@
// / ___ \ | | | (__ | | | || | ___) || |_| __/| (_| || | | | | || _|| (_| || | | | | | | |
// /_/ \_\|_| \___||_| |_||_||____/ \__|\___| \__,_||_| |_| |_||_| \__,_||_| |_| |_| |_|
// |
// 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,10 +21,10 @@
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.Linq;
using System.Net;
@@ -39,6 +39,7 @@ using ArchiSteamFarm.Storage;
using Humanizer;
using Humanizer.Localisation;
using JetBrains.Annotations;
using Microsoft.IdentityModel.JsonWebTokens;
using SteamKit2;
using Zxcvbn;
@@ -48,9 +49,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) {
@@ -91,7 +90,7 @@ public static class Utilities {
CookieCollection cookies = cookieContainer.GetCookies(uri);
return cookies.Count > 0 ? cookies.FirstOrDefault(cookie => cookie.Name == name)?.Value : null;
return cookies.FirstOrDefault(cookie => cookie.Name == name)?.Value;
}
[PublicAPI]
@@ -123,7 +122,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 +178,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,6 +246,23 @@ public static class Utilities {
return job.ToTask();
}
[PublicAPI]
public static bool TryReadJsonWebToken(string token, [NotNullWhen(true)] out JsonWebToken? result) {
ArgumentException.ThrowIfNullOrEmpty(token);
try {
result = new JsonWebToken(token);
} catch (Exception e) {
ASF.ArchiLogger.LogGenericDebuggingException(e);
result = null;
return false;
}
return true;
}
internal static void DeleteEmptyDirectoriesRecursively(string directory) {
ArgumentException.ThrowIfNullOrEmpty(directory);

View File

@@ -4,7 +4,7 @@
// / ___ \ | | | (__ | | | || | ___) || |_| __/| (_| || | | | | || _|| (_| || | | | | | | |
// /_/ \_\|_| \___||_| |_||_||____/ \__|\___| \__,_||_| |_| |_||_| \__,_||_| |_| |_| |_|
// |
// 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");
@@ -35,7 +35,7 @@ public sealed class ArchiCacheable<T> : IDisposable {
private bool IsInitialized => InitializedAt > DateTime.MinValue;
private bool IsPermanentCache => CacheLifetime == Timeout.InfiniteTimeSpan;
private bool IsRecent => IsPermanentCache || (DateTime.UtcNow.Subtract(InitializedAt) < CacheLifetime);
private bool IsRecent => IsInitialized && (IsPermanentCache || (DateTime.UtcNow.Subtract(InitializedAt) < CacheLifetime));
private DateTime InitializedAt;
private T? InitializedValue;
@@ -55,7 +55,7 @@ public sealed class ArchiCacheable<T> : IDisposable {
throw new InvalidEnumArgumentException(nameof(cacheFallback), (int) cacheFallback, typeof(ECacheFallback));
}
if (IsInitialized && IsRecent) {
if (IsRecent) {
return (true, InitializedValue);
}
@@ -68,7 +68,7 @@ public sealed class ArchiCacheable<T> : IDisposable {
}
try {
if (IsInitialized && IsRecent) {
if (IsRecent) {
return (true, InitializedValue);
}

View File

@@ -4,7 +4,7 @@
// / ___ \ | | | (__ | | | || | ___) || |_| __/| (_| || | | | | || _|| (_| || | | | | | | |
// /_/ \_\|_| \___||_| |_||_||____/ \__|\___| \__,_||_| |_| |_||_| \__,_||_| |_| |_| |_|
// |
// 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 +20,8 @@
// limitations under the License.
using System;
using System.Collections.Frozen;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.ComponentModel;
using System.Globalization;
using System.IO;
@@ -45,7 +45,7 @@ public static class ArchiCryptoHelper {
internal static bool HasDefaultCryptKey { get; private set; } = true;
private static readonly ImmutableHashSet<string> ForbiddenCryptKeyPhrases = ImmutableHashSet.Create(StringComparer.InvariantCultureIgnoreCase, "crypt", "key", "cryptkey");
private static readonly FrozenSet<string> ForbiddenCryptKeyPhrases = new HashSet<string>(3, StringComparer.InvariantCultureIgnoreCase) { "crypt", "key", "cryptkey" }.ToFrozenSet(StringComparer.InvariantCultureIgnoreCase);
private static IEnumerable<byte> SteamParentalCharacters => Enumerable.Range('0', 10).Select(static character => (byte) character);
@@ -59,53 +59,53 @@ public static class ArchiCryptoHelper {
private static byte[] EncryptionKey = Encoding.UTF8.GetBytes(nameof(ArchiSteamFarm));
internal static async Task<string?> Decrypt(ECryptoMethod cryptoMethod, string encryptedString) {
internal static async Task<string?> Decrypt(ECryptoMethod cryptoMethod, string text) {
if (!Enum.IsDefined(cryptoMethod)) {
throw new InvalidEnumArgumentException(nameof(cryptoMethod), (int) cryptoMethod, typeof(ECryptoMethod));
}
ArgumentException.ThrowIfNullOrEmpty(encryptedString);
ArgumentException.ThrowIfNullOrEmpty(text);
return cryptoMethod switch {
ECryptoMethod.AES => DecryptAES(encryptedString),
ECryptoMethod.EnvironmentVariable => Environment.GetEnvironmentVariable(encryptedString)?.Trim(),
ECryptoMethod.File => await ReadFromFile(encryptedString).ConfigureAwait(false),
ECryptoMethod.PlainText => encryptedString,
ECryptoMethod.ProtectedDataForCurrentUser => DecryptProtectedDataForCurrentUser(encryptedString),
ECryptoMethod.AES => DecryptAES(text),
ECryptoMethod.EnvironmentVariable => Environment.GetEnvironmentVariable(text)?.Trim(),
ECryptoMethod.File => await ReadFromFile(text).ConfigureAwait(false),
ECryptoMethod.PlainText => text,
ECryptoMethod.ProtectedDataForCurrentUser => DecryptProtectedDataForCurrentUser(text),
_ => throw new InvalidOperationException(nameof(cryptoMethod))
};
}
internal static string? Encrypt(ECryptoMethod cryptoMethod, string decryptedString) {
internal static string? Encrypt(ECryptoMethod cryptoMethod, string text) {
if (!Enum.IsDefined(cryptoMethod)) {
throw new InvalidEnumArgumentException(nameof(cryptoMethod), (int) cryptoMethod, typeof(ECryptoMethod));
}
ArgumentException.ThrowIfNullOrEmpty(decryptedString);
ArgumentException.ThrowIfNullOrEmpty(text);
return cryptoMethod switch {
ECryptoMethod.AES => EncryptAES(decryptedString),
ECryptoMethod.EnvironmentVariable => decryptedString,
ECryptoMethod.File => decryptedString,
ECryptoMethod.PlainText => decryptedString,
ECryptoMethod.ProtectedDataForCurrentUser => EncryptProtectedDataForCurrentUser(decryptedString),
ECryptoMethod.AES => EncryptAES(text),
ECryptoMethod.EnvironmentVariable => text,
ECryptoMethod.File => text,
ECryptoMethod.PlainText => text,
ECryptoMethod.ProtectedDataForCurrentUser => EncryptProtectedDataForCurrentUser(text),
_ => throw new InvalidOperationException(nameof(cryptoMethod))
};
}
internal static string Hash(EHashingMethod hashingMethod, string stringToHash) {
internal static string Hash(EHashingMethod hashingMethod, string text) {
if (!Enum.IsDefined(hashingMethod)) {
throw new InvalidEnumArgumentException(nameof(hashingMethod), (int) hashingMethod, typeof(EHashingMethod));
}
ArgumentException.ThrowIfNullOrEmpty(stringToHash);
ArgumentException.ThrowIfNullOrEmpty(text);
if (hashingMethod == EHashingMethod.PlainText) {
return stringToHash;
return text;
}
byte[] passwordBytes = Encoding.UTF8.GetBytes(stringToHash);
byte[] hashBytes = Hash(passwordBytes, EncryptionKey, DefaultHashLength, hashingMethod);
byte[] textBytes = Encoding.UTF8.GetBytes(text);
byte[] hashBytes = Hash(textBytes, EncryptionKey, DefaultHashLength, hashingMethod);
return Convert.ToBase64String(hashBytes);
}
@@ -197,13 +197,31 @@ public static class ArchiCryptoHelper {
EncryptionKey = encryptionKey;
}
private static string? DecryptAES(string encryptedString) {
ArgumentException.ThrowIfNullOrEmpty(encryptedString);
internal static bool VerifyHash(EHashingMethod hashingMethod, string text, string hash) {
if (!Enum.IsDefined(hashingMethod)) {
throw new InvalidEnumArgumentException(nameof(hashingMethod), (int) hashingMethod, typeof(EHashingMethod));
}
ArgumentException.ThrowIfNullOrEmpty(text);
ArgumentException.ThrowIfNullOrEmpty(hash);
// Text is always provided as plain text
byte[] textBytes = Encoding.UTF8.GetBytes(text);
textBytes = Hash(textBytes, EncryptionKey, DefaultHashLength, hashingMethod);
// Hash is either plain text password (when EHashingMethod.PlainText), or base64-encoded hash
byte[] hashBytes = hashingMethod == EHashingMethod.PlainText ? Encoding.UTF8.GetBytes(hash) : Convert.FromBase64String(hash);
return CryptographicOperations.FixedTimeEquals(textBytes, hashBytes);
}
private static string? DecryptAES(string text) {
ArgumentException.ThrowIfNullOrEmpty(text);
try {
byte[] key = SHA256.HashData(EncryptionKey);
byte[] decryptedData = Convert.FromBase64String(encryptedString);
byte[] decryptedData = Convert.FromBase64String(text);
decryptedData = CryptoHelper.SymmetricDecrypt(decryptedData, key);
return Encoding.UTF8.GetString(decryptedData);
@@ -214,8 +232,8 @@ public static class ArchiCryptoHelper {
}
}
private static string? DecryptProtectedDataForCurrentUser(string encryptedString) {
ArgumentException.ThrowIfNullOrEmpty(encryptedString);
private static string? DecryptProtectedDataForCurrentUser(string text) {
ArgumentException.ThrowIfNullOrEmpty(text);
if (!OperatingSystem.IsWindows()) {
return null;
@@ -223,7 +241,7 @@ public static class ArchiCryptoHelper {
try {
byte[] decryptedData = ProtectedData.Unprotect(
Convert.FromBase64String(encryptedString),
Convert.FromBase64String(text),
EncryptionKey,
DataProtectionScope.CurrentUser
);
@@ -236,13 +254,13 @@ public static class ArchiCryptoHelper {
}
}
private static string? EncryptAES(string decryptedString) {
ArgumentException.ThrowIfNullOrEmpty(decryptedString);
private static string? EncryptAES(string text) {
ArgumentException.ThrowIfNullOrEmpty(text);
try {
byte[] key = SHA256.HashData(EncryptionKey);
byte[] encryptedData = Encoding.UTF8.GetBytes(decryptedString);
byte[] encryptedData = Encoding.UTF8.GetBytes(text);
encryptedData = CryptoHelper.SymmetricEncrypt(encryptedData, key);
return Convert.ToBase64String(encryptedData);
@@ -253,8 +271,8 @@ public static class ArchiCryptoHelper {
}
}
private static string? EncryptProtectedDataForCurrentUser(string decryptedString) {
ArgumentException.ThrowIfNullOrEmpty(decryptedString);
private static string? EncryptProtectedDataForCurrentUser(string text) {
ArgumentException.ThrowIfNullOrEmpty(text);
if (!OperatingSystem.IsWindows()) {
return null;
@@ -262,7 +280,7 @@ public static class ArchiCryptoHelper {
try {
byte[] encryptedData = ProtectedData.Protect(
Encoding.UTF8.GetBytes(decryptedString),
Encoding.UTF8.GetBytes(text),
EncryptionKey,
DataProtectionScope.CurrentUser
);

View File

@@ -4,7 +4,7 @@
// / ___ \ | | | (__ | | | || | ___) || |_| __/| (_| || | | | | || _|| (_| || | | | | | | |
// /_/ \_\|_| \___||_| |_||_||____/ \__|\___| \__,_||_| |_| |_||_| \__,_||_| |_| |_| |_|
// |
// 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

@@ -4,7 +4,7 @@
// / ___ \ | | | (__ | | | || | ___) || |_| __/| (_| || | | | | || _|| (_| || | | | | | | |
// /_/ \_\|_| \___||_| |_||_||____/ \__|\___| \__,_||_| |_| |_||_| \__,_||_| |_| |_| |_|
// |
// 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

@@ -4,7 +4,7 @@
// / ___ \ | | | (__ | | | || | ___) || |_| __/| (_| || | | | | || _|| (_| || | | | | | | |
// /_/ \_\|_| \___||_| |_||_||____/ \__|\___| \__,_||_| |_| |_||_| \__,_||_| |_| |_| |_|
// |
// 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

@@ -0,0 +1,30 @@
// _ _ _ ____ _ _____
// / \ _ __ ___ | |__ (_)/ ___| | |_ ___ __ _ _ __ ___ | ___|__ _ _ __ _ __ ___
// / _ \ | '__|/ __|| '_ \ | |\___ \ | __|/ _ \ / _` || '_ ` _ \ | |_ / _` || '__|| '_ ` _ \
// / ___ \ | | | (__ | | | || | ___) || |_| __/| (_| || | | | | || _|| (_| || | | | | | | |
// /_/ \_\|_| \___||_| |_||_||____/ \__|\___| \__,_||_| |_| |_||_| \__,_||_| |_| |_| |_|
// |
// Copyright 2015-2024 Łukasz "JustArchi" Domeradzki
// Contact: JustArchi@JustArchi.net
// |
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
// |
// http://www.apache.org/licenses/LICENSE-2.0
// |
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
using System;
using System.Text.Json.Serialization;
using JetBrains.Annotations;
namespace ArchiSteamFarm.Helpers.Json;
[AttributeUsage(AttributeTargets.Field | AttributeTargets.Property)]
[PublicAPI]
public sealed class JsonDisallowNullAttribute : JsonAttribute;

View File

@@ -0,0 +1,186 @@
// _ _ _ ____ _ _____
// / \ _ __ ___ | |__ (_)/ ___| | |_ ___ __ _ _ __ ___ | ___|__ _ _ __ _ __ ___
// / _ \ | '__|/ __|| '_ \ | |\___ \ | __|/ _ \ / _` || '_ ` _ \ | |_ / _` || '__|| '_ ` _ \
// / ___ \ | | | (__ | | | || | ___) || |_| __/| (_| || | | | | || _|| (_| || | | | | | | |
// /_/ \_\|_| \___||_| |_||_||____/ \__|\___| \__,_||_| |_| |_||_| \__,_||_| |_| |_| |_|
// |
// 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.Diagnostics.CodeAnalysis;
using System.Globalization;
using System.IO;
using System.Linq;
using System.Reflection;
using System.Text.Json;
using System.Text.Json.Serialization.Metadata;
using System.Threading;
using System.Threading.Tasks;
using ArchiSteamFarm.Localization;
using JetBrains.Annotations;
namespace ArchiSteamFarm.Helpers.Json;
public static class JsonUtilities {
[PublicAPI]
public static readonly JsonSerializerOptions DefaultJsonSerialierOptions = CreateDefaultJsonSerializerOptions();
[PublicAPI]
public static readonly JsonSerializerOptions IndentedJsonSerialierOptions = CreateDefaultJsonSerializerOptions(true);
[PublicAPI]
public static JsonElement ToJsonElement<T>(this T obj) where T : notnull {
ArgumentNullException.ThrowIfNull(obj);
return JsonSerializer.SerializeToElement(obj, DefaultJsonSerialierOptions);
}
[PublicAPI]
public static T? ToJsonObject<T>(this JsonElement jsonElement, CancellationToken cancellationToken = default) => jsonElement.Deserialize<T>(DefaultJsonSerialierOptions);
[PublicAPI]
public static async ValueTask<T?> ToJsonObject<T>(this Stream stream, CancellationToken cancellationToken = default) {
ArgumentNullException.ThrowIfNull(stream);
return await JsonSerializer.DeserializeAsync<T>(stream, DefaultJsonSerialierOptions, cancellationToken).ConfigureAwait(false);
}
[PublicAPI]
public static T? ToJsonObject<T>([StringSyntax(StringSyntaxAttribute.Json)] this string json) {
ArgumentException.ThrowIfNullOrEmpty(json);
return JsonSerializer.Deserialize<T>(json, DefaultJsonSerialierOptions);
}
[PublicAPI]
public static string ToJsonText<T>(this T obj, bool writeIndented = false) => JsonSerializer.Serialize(obj, writeIndented ? IndentedJsonSerialierOptions : DefaultJsonSerialierOptions);
private static void ApplyCustomModifiers(JsonTypeInfo jsonTypeInfo) {
ArgumentNullException.ThrowIfNull(jsonTypeInfo);
bool potentialDisallowedNullsPossible = false;
foreach (JsonPropertyInfo property in jsonTypeInfo.Properties) {
// All our modifications require a valid Get method on a property
if (property.Get == null) {
continue;
}
// The object should be validated against potential nulls if at least one property has [JsonDisallowNull] declared, avoid performance penalty otherwise
if (property.AttributeProvider?.IsDefined(typeof(JsonDisallowNullAttribute), false) == true) {
if (property.PropertyType.IsValueType && (Nullable.GetUnderlyingType(property.PropertyType) == null)) {
// We should have no [JsonDisallowNull] declared on non-nullable types, this requires developer correction
throw new InvalidOperationException(string.Format(CultureInfo.CurrentCulture, Strings.WarningUnknownValuePleaseReport, nameof(JsonDisallowNullAttribute), $"{property.Name} ({jsonTypeInfo.Type})"));
}
potentialDisallowedNullsPossible = true;
}
// The property should be checked against ShouldSerialize if there is a valid method to invoke, avoid performance penalty otherwise
MethodInfo? shouldSerializeMethod = GetShouldSerializeMethod(jsonTypeInfo.Type, property);
if (shouldSerializeMethod != null) {
property.ShouldSerialize = (parent, _) => ShouldSerialize(shouldSerializeMethod, parent);
}
}
if (potentialDisallowedNullsPossible) {
jsonTypeInfo.OnDeserialized = OnPotentialDisallowedNullsDeserialized;
}
}
private static JsonSerializerOptions CreateDefaultJsonSerializerOptions(bool writeIndented = false) =>
new() {
AllowTrailingCommas = true,
PropertyNamingPolicy = null,
ReadCommentHandling = JsonCommentHandling.Skip,
TypeInfoResolver = new DefaultJsonTypeInfoResolver { Modifiers = { ApplyCustomModifiers } },
WriteIndented = writeIndented
};
[UnconditionalSuppressMessage("AssemblyLoadTrimming", "IL2070", Justification = "We don't care about trimmed methods, it's not like we can make it work differently anyway")]
[UnconditionalSuppressMessage("AssemblyLoadTrimming", "IL2075", Justification = "We don't care about trimmed properties, it's not like we can make it work differently anyway")]
private static MethodInfo? GetShouldSerializeMethod([SuppressMessage("ReSharper", "SuggestBaseTypeForParameter")] Type parent, JsonPropertyInfo property) {
ArgumentNullException.ThrowIfNull(parent);
ArgumentNullException.ThrowIfNull(property);
// Handle most common case where ShouldSerializeXYZ() matches property name
MethodInfo? result = parent.GetMethod($"ShouldSerialize{property.Name}", BindingFlags.Instance | BindingFlags.NonPublic | BindingFlags.Public | BindingFlags.Static, null, Type.EmptyTypes, null);
if (result?.ReturnType == typeof(bool)) {
// Method exists and returns a boolean, that's what we'd like to hear
return result;
}
// Handle less common case where ShouldSerializeXYZ() matches original member name
PropertyInfo? memberNameProperty = property.GetType().GetProperty("MemberName", BindingFlags.Instance | BindingFlags.NonPublic | BindingFlags.Public);
if (memberNameProperty == null) {
// Should never happen, investigate if it does
throw new InvalidOperationException(nameof(memberNameProperty));
}
object? memberNameResult = memberNameProperty.GetValue(property);
if (memberNameResult is not string memberName) {
// Should never happen, investigate if it does
throw new InvalidOperationException(nameof(memberName));
}
if (string.IsNullOrEmpty(memberName) || (memberName == property.Name)) {
// We don't have anything to work with further, there is no ShouldSerialize() method
return null;
}
result = parent.GetMethod($"ShouldSerialize{memberName}", BindingFlags.Instance | BindingFlags.NonPublic | BindingFlags.Public | BindingFlags.Static, null, Type.EmptyTypes, null);
// Use alternative method if it exists and returns a boolean
return result?.ReturnType == typeof(bool) ? result : null;
}
[UnconditionalSuppressMessage("AssemblyLoadTrimming", "IL2075", Justification = "We don't care about trimmed properties, it's not like we can make it work differently anyway")]
private static void OnPotentialDisallowedNullsDeserialized(object obj) {
ArgumentNullException.ThrowIfNull(obj);
Type type = obj.GetType();
foreach (FieldInfo field in type.GetFields(BindingFlags.Instance | BindingFlags.NonPublic | BindingFlags.Public | BindingFlags.Static).Where(field => field.IsDefined(typeof(JsonDisallowNullAttribute), false) && (field.GetValue(obj) == null))) {
throw new JsonException($"Required field {field.Name} expects a non-null value.");
}
foreach (PropertyInfo property in type.GetProperties(BindingFlags.Instance | BindingFlags.NonPublic | BindingFlags.Public | BindingFlags.Static).Where(property => (property.GetMethod != null) && property.IsDefined(typeof(JsonDisallowNullAttribute), false) && (property.GetValue(obj) == null))) {
throw new JsonException($"Required property {property.Name} expects a non-null value.");
}
}
private static bool ShouldSerialize(MethodInfo shouldSerializeMethod, object parent) {
ArgumentNullException.ThrowIfNull(shouldSerializeMethod);
ArgumentNullException.ThrowIfNull(parent);
if (shouldSerializeMethod.ReturnType != typeof(bool)) {
throw new InvalidOperationException(nameof(shouldSerializeMethod));
}
object? shouldSerialize = shouldSerializeMethod.Invoke(parent, null);
if (shouldSerialize is not bool result) {
// Should not happen, we've already determined we have a method that returns a boolean
throw new InvalidOperationException(nameof(shouldSerialize));
}
return result;
}
}

View File

@@ -4,7 +4,7 @@
// / ___ \ | | | (__ | | | || | ___) || |_| __/| (_| || | | | | || _|| (_| || | | | | | | |
// /_/ \_\|_| \___||_| |_||_||____/ \__|\___| \__,_||_| |_| |_||_| \__,_||_| |_| |_| |_|
// |
// 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

@@ -4,7 +4,7 @@
// / ___ \ | | | (__ | | | || | ___) || |_| __/| (_| || | | | | || _|| (_| || | | | | | | |
// /_/ \_\|_| \___||_| |_||_||____/ \__|\___| \__,_||_| |_| |_||_| \__,_||_| |_| |_| |_|
// |
// 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,7 +24,8 @@ using System.IO;
using System.Threading;
using System.Threading.Tasks;
using ArchiSteamFarm.Core;
using Newtonsoft.Json;
using ArchiSteamFarm.Helpers.Json;
using JetBrains.Annotations;
namespace ArchiSteamFarm.Helpers;
@@ -49,47 +50,60 @@ public abstract class SerializableFile : IDisposable {
}
}
protected async Task Save() {
if (string.IsNullOrEmpty(FilePath)) {
/// <summary>
/// Implementing this method in your target class is crucial for providing supported functionality.
/// In order to do so, it's enough to call static <see cref="Save" /> function from the parent class, providing <code>this</code> as input parameter.
/// Afterwards, simply call your <see cref="Save" /> function whenever you need to save changes.
/// This approach will allow JSON serializer used in the <see cref="SerializableFile" /> to properly discover all of the properties used in your class.
/// Unfortunately, due to STJ's limitations, called by some "security", it's not possible for base class to resolve your properties automatically otherwise.
/// </summary>
/// <example>protected override Task Save() => Save(this);</example>
[UsedImplicitly]
protected abstract Task Save();
protected static async Task Save<T>(T serializableFile) where T : SerializableFile {
ArgumentNullException.ThrowIfNull(serializableFile);
if (string.IsNullOrEmpty(serializableFile.FilePath)) {
throw new InvalidOperationException(nameof(FilePath));
}
if (ReadOnly) {
if (serializableFile.ReadOnly) {
return;
}
// ReSharper disable once SuspiciousLockOverSynchronizationPrimitive - this is not a mistake, we need extra synchronization, and we can re-use the semaphore object for that
lock (FileSemaphore) {
if (SavingScheduled) {
lock (serializableFile.FileSemaphore) {
if (serializableFile.SavingScheduled) {
return;
}
SavingScheduled = true;
serializableFile.SavingScheduled = true;
}
await FileSemaphore.WaitAsync().ConfigureAwait(false);
await serializableFile.FileSemaphore.WaitAsync().ConfigureAwait(false);
try {
// ReSharper disable once SuspiciousLockOverSynchronizationPrimitive - this is not a mistake, we need extra synchronization, and we can re-use the semaphore object for that
lock (FileSemaphore) {
SavingScheduled = false;
lock (serializableFile.FileSemaphore) {
serializableFile.SavingScheduled = false;
}
if (ReadOnly) {
if (serializableFile.ReadOnly) {
return;
}
string json = JsonConvert.SerializeObject(this, Debugging.IsUserDebugging ? Formatting.Indented : Formatting.None);
string json = serializableFile.ToJsonText(Debugging.IsUserDebugging);
if (string.IsNullOrEmpty(json)) {
throw new InvalidOperationException(nameof(json));
}
// We always want to write entire content to temporary file first, in order to never load corrupted data, also when target file doesn't exist
string newFilePath = $"{FilePath}.new";
string newFilePath = $"{serializableFile.FilePath}.new";
if (File.Exists(FilePath)) {
string currentJson = await File.ReadAllTextAsync(FilePath).ConfigureAwait(false);
if (File.Exists(serializableFile.FilePath)) {
string currentJson = await File.ReadAllTextAsync(serializableFile.FilePath).ConfigureAwait(false);
if (json == currentJson) {
return;
@@ -97,16 +111,16 @@ public abstract class SerializableFile : IDisposable {
await File.WriteAllTextAsync(newFilePath, json).ConfigureAwait(false);
File.Replace(newFilePath, FilePath, null);
File.Replace(newFilePath, serializableFile.FilePath, null);
} else {
await File.WriteAllTextAsync(newFilePath, json).ConfigureAwait(false);
File.Move(newFilePath, FilePath);
File.Move(newFilePath, serializableFile.FilePath);
}
} catch (Exception e) {
ASF.ArchiLogger.LogGenericException(e);
} finally {
FileSemaphore.Release();
serializableFile.FileSemaphore.Release();
}
}

View File

@@ -4,7 +4,7 @@
// / ___ \ | | | (__ | | | || | ___) || |_| __/| (_| || | | | | || _|| (_| || | | | | | | |
// /_/ \_\|_| \___||_| |_||_||____/ \__|\___| \__,_||_| |_| |_||_| \__,_||_| |_| |_| |_|
// |
// 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 +21,10 @@
using System;
using System.IO;
using System.Text.Json.Nodes;
using System.Threading.Tasks;
using ArchiSteamFarm.Core;
using ArchiSteamFarm.Helpers.Json;
using ArchiSteamFarm.IPC.Controllers.Api;
using ArchiSteamFarm.Localization;
using ArchiSteamFarm.NLog;
@@ -31,8 +33,6 @@ using Microsoft.AspNetCore.Hosting;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using NLog.Web;
namespace ArchiSteamFarm.IPC;
@@ -83,9 +83,9 @@ internal static class ArchiKestrel {
string json = await File.ReadAllTextAsync(customConfigPath).ConfigureAwait(false);
if (!string.IsNullOrEmpty(json)) {
JObject jObject = JObject.Parse(json);
JsonNode? jsonNode = JsonNode.Parse(json);
ASF.ArchiLogger.LogGenericDebug($"{SharedInfo.IPCConfigFile}: {jObject.ToString(Formatting.Indented)}");
ASF.ArchiLogger.LogGenericDebug($"{SharedInfo.IPCConfigFile}: {jsonNode?.ToJsonText(true) ?? "null"}");
}
} catch (Exception e) {
ASF.ArchiLogger.LogGenericException(e);

View File

@@ -4,7 +4,7 @@
// / ___ \ | | | (__ | | | || | ___) || |_| __/| (_| || | | | | || _|| (_| || | | | | | | |
// /_/ \_\|_| \___||_| |_||_||____/ \__|\___| \__,_||_| |_| |_||_| \__,_||_| |_| |_| |_|
// |
// 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,6 +24,7 @@ using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using System.Net;
using System.Text.Json;
using System.Threading.Tasks;
using ArchiSteamFarm.Core;
using ArchiSteamFarm.IPC.Requests;
@@ -32,7 +33,6 @@ using ArchiSteamFarm.Localization;
using ArchiSteamFarm.Steam.Interaction;
using ArchiSteamFarm.Storage;
using Microsoft.AspNetCore.Mvc;
using Newtonsoft.Json.Linq;
namespace ArchiSteamFarm.IPC.Controllers.Api;
@@ -124,9 +124,9 @@ public sealed class ASFController : ArchiController {
}
if (ASF.GlobalConfig.AdditionalProperties is { Count: > 0 }) {
request.GlobalConfig.AdditionalProperties ??= new Dictionary<string, JToken>(ASF.GlobalConfig.AdditionalProperties.Count, ASF.GlobalConfig.AdditionalProperties.Comparer);
request.GlobalConfig.AdditionalProperties ??= new Dictionary<string, JsonElement>(ASF.GlobalConfig.AdditionalProperties.Count, ASF.GlobalConfig.AdditionalProperties.Comparer);
foreach ((string key, JToken value) in ASF.GlobalConfig.AdditionalProperties.Where(property => !request.GlobalConfig.AdditionalProperties.ContainsKey(property.Key))) {
foreach ((string key, JsonElement value) in ASF.GlobalConfig.AdditionalProperties.Where(property => !request.GlobalConfig.AdditionalProperties.ContainsKey(property.Key))) {
request.GlobalConfig.AdditionalProperties.Add(key, value);
}

View File

@@ -4,7 +4,7 @@
// / ___ \ | | | (__ | | | || | ___) || |_| __/| (_| || | | | | || _|| (_| || | | | | | | |
// /_/ \_\|_| \___||_| |_||_||____/ \__|\___| \__,_||_| |_| |_||_| \__,_||_| |_| |_| |_|
// |
// 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");
@@ -35,4 +35,4 @@ namespace ArchiSteamFarm.IPC.Controllers.Api;
[SwaggerResponse((int) HttpStatusCode.Forbidden, $"ASF lacks {nameof(GlobalConfig.IPCPassword)} and you're not permitted to access the API, or {nameof(GlobalConfig.IPCPassword)} is set and you've failed to authenticate too many times (try again in an hour). See {SharedInfo.ProjectURL}/wiki/IPC#authentication.", typeof(GenericResponse<StatusCodeResponse>))]
[SwaggerResponse((int) HttpStatusCode.InternalServerError, "ASF has encountered an unexpected error while serving the request. The log may include extra info related to this issue.")]
[SwaggerResponse((int) HttpStatusCode.ServiceUnavailable, "ASF has encountered an error while requesting a third-party resource. Try again later.")]
public abstract class ArchiController : ControllerBase { }
public abstract class ArchiController : ControllerBase;

View File

@@ -4,7 +4,7 @@
// / ___ \ | | | (__ | | | || | ___) || |_| __/| (_| || | | | | || _|| (_| || | | | | | | |
// /_/ \_\|_| \___||_| |_||_||____/ \__|\___| \__,_||_| |_| |_||_| \__,_||_| |_| |_| |_|
// |
// 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");
@@ -25,6 +25,7 @@ using System.Collections.Specialized;
using System.Globalization;
using System.Linq;
using System.Net;
using System.Text.Json;
using System.Threading.Tasks;
using ArchiSteamFarm.Core;
using ArchiSteamFarm.IPC.Requests;
@@ -33,7 +34,6 @@ using ArchiSteamFarm.Localization;
using ArchiSteamFarm.Steam;
using ArchiSteamFarm.Steam.Storage;
using Microsoft.AspNetCore.Mvc;
using Newtonsoft.Json.Linq;
using SteamKit2.Internal;
namespace ArchiSteamFarm.IPC.Controllers.Api;
@@ -127,9 +127,9 @@ public sealed class BotController : ArchiController {
}
if (bot.BotConfig.AdditionalProperties?.Count > 0) {
request.BotConfig.AdditionalProperties ??= new Dictionary<string, JToken>(bot.BotConfig.AdditionalProperties.Count, bot.BotConfig.AdditionalProperties.Comparer);
request.BotConfig.AdditionalProperties ??= new Dictionary<string, JsonElement>(bot.BotConfig.AdditionalProperties.Count, bot.BotConfig.AdditionalProperties.Comparer);
foreach ((string key, JToken value) in bot.BotConfig.AdditionalProperties.Where(property => !request.BotConfig.AdditionalProperties.ContainsKey(property.Key))) {
foreach ((string key, JsonElement value) in bot.BotConfig.AdditionalProperties.Where(property => !request.BotConfig.AdditionalProperties.ContainsKey(property.Key))) {
request.BotConfig.AdditionalProperties.Add(key, value);
}

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