mirror of
https://github.com/JustArchiNET/ArchiSteamFarm.git
synced 2025-12-16 06:20:34 +00:00
Compare commits
404 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4c0a5b7553 | ||
|
|
533fbe0c2f | ||
|
|
437dfd5f02 | ||
|
|
5f4962ddcc | ||
|
|
7efa609a13 | ||
|
|
0d7049ea7c | ||
|
|
997e7f0420 | ||
|
|
96619b9565 | ||
|
|
4c2a786e54 | ||
|
|
469b1571e1 | ||
|
|
3bc66e0d27 | ||
|
|
9a4fac600a | ||
|
|
546ba8b68f | ||
|
|
ed29c0633f | ||
|
|
35b1135b50 | ||
|
|
90403c98c9 | ||
|
|
71a1c0574a | ||
|
|
ba64a50bb5 | ||
|
|
05131b2b76 | ||
|
|
0894eaee28 | ||
|
|
6f2fd4eccc | ||
|
|
04b534bda1 | ||
|
|
3620796d6d | ||
|
|
a321a38ccd | ||
|
|
ea965dfa85 | ||
|
|
f381106de2 | ||
|
|
b9ab3d6490 | ||
|
|
4cf1d1e08b | ||
|
|
cba6e4df64 | ||
|
|
0dd6f38748 | ||
|
|
970ba437a0 | ||
|
|
e0bbbe3894 | ||
|
|
504791b5b6 | ||
|
|
6a6d0b48b9 | ||
|
|
84ff83bbe2 | ||
|
|
787bcc3546 | ||
|
|
fd811d8cf4 | ||
|
|
3fa743f64b | ||
|
|
ec374c050a | ||
|
|
5a07f8a2a3 | ||
|
|
91b09dc43f | ||
|
|
8d6d355a3b | ||
|
|
da6fd69398 | ||
|
|
0e623cfd15 | ||
|
|
1c01d8f59f | ||
|
|
5723ee7b19 | ||
|
|
1d85292451 | ||
|
|
0d65f5174b | ||
|
|
b7f34f0d5d | ||
|
|
4ffcea72b0 | ||
|
|
9c5fade596 | ||
|
|
331ecc1cc8 | ||
|
|
6428e5abd1 | ||
|
|
b86f83a634 | ||
|
|
ff55e09783 | ||
|
|
d7d24d5e47 | ||
|
|
48a14136a9 | ||
|
|
c9acbb7bf2 | ||
|
|
f98a159799 | ||
|
|
184232995d | ||
|
|
aedede3ba4 | ||
|
|
c874779c0d | ||
|
|
4a2457d571 | ||
|
|
65fdd4196a | ||
|
|
19eefff525 | ||
|
|
626c18ba23 | ||
|
|
d9c8dc1e2d | ||
|
|
25fa5cabbb | ||
|
|
21c9dac593 | ||
|
|
617dccbd9a | ||
|
|
2f2665e2ad | ||
|
|
fcc0d70cd1 | ||
|
|
06b2cf4ff5 | ||
|
|
8642b0775e | ||
|
|
c193858973 | ||
|
|
362e921a27 | ||
|
|
e21fa45718 | ||
|
|
caae270ea8 | ||
|
|
4843d539f0 | ||
|
|
fa8e649288 | ||
|
|
20708ad900 | ||
|
|
5e0b9551da | ||
|
|
e43adf4de7 | ||
|
|
afe16b8eb1 | ||
|
|
e9f9663714 | ||
|
|
d514157903 | ||
|
|
56490f2d86 | ||
|
|
1fd6c8a477 | ||
|
|
e7bdd408be | ||
|
|
f73b4737b6 | ||
|
|
14c905b8ef | ||
|
|
fa7849460c | ||
|
|
4d12c9117f | ||
|
|
e2f08fe35b | ||
|
|
0089a87018 | ||
|
|
6325c454bc | ||
|
|
6b1f64579a | ||
|
|
14cfd61615 | ||
|
|
f2a8768f80 | ||
|
|
556f3fdac0 | ||
|
|
59124fcf68 | ||
|
|
71643446db | ||
|
|
a93ca58e9c | ||
|
|
f12e87c2f4 | ||
|
|
d48c96604b | ||
|
|
0976bbfd2d | ||
|
|
6f66518607 | ||
|
|
e4c20df4a8 | ||
|
|
5135677360 | ||
|
|
f6004f558b | ||
|
|
ddf08c4dc0 | ||
|
|
388eaf614d | ||
|
|
3a0768f9ef | ||
|
|
cb0e04c022 | ||
|
|
55cdac205d | ||
|
|
1bc0d6f16c | ||
|
|
f7377a7043 | ||
|
|
76f1ad45dd | ||
|
|
f21ffde803 | ||
|
|
e9c96f175f | ||
|
|
0f9a4c7c31 | ||
|
|
87451615e8 | ||
|
|
fa19aaae2e | ||
|
|
6b8280fceb | ||
|
|
7a13895429 | ||
|
|
75668ea099 | ||
|
|
d3aa881f55 | ||
|
|
33a1fdf556 | ||
|
|
0c848ec366 | ||
|
|
21d4e46b81 | ||
|
|
7a21c7bc45 | ||
|
|
9358aa5d1f | ||
|
|
6c9e9da740 | ||
|
|
a78a607f73 | ||
|
|
879323ed20 | ||
|
|
4e081b26a1 | ||
|
|
6cd3459dd4 | ||
|
|
482576f16d | ||
|
|
f63dbffee3 | ||
|
|
d9cdc806fe | ||
|
|
36a78b55a4 | ||
|
|
ab983099cc | ||
|
|
aab8f5f0b5 | ||
|
|
6b0bf0f9c1 | ||
|
|
3968130e15 | ||
|
|
dd488a1c9c | ||
|
|
de3c332803 | ||
|
|
684b7b6e28 | ||
|
|
d36f16e205 | ||
|
|
c3ab3d767f | ||
|
|
a790c34976 | ||
|
|
be208eab92 | ||
|
|
c9598709d6 | ||
|
|
367e7f841c | ||
|
|
8c988cfed6 | ||
|
|
5b15535661 | ||
|
|
e0c692e0ab | ||
|
|
1ddf6b34e2 | ||
|
|
8476c8c221 | ||
|
|
24ec938565 | ||
|
|
0dbfba275e | ||
|
|
55c5c70b52 | ||
|
|
17aa8297da | ||
|
|
b0aa2a9104 | ||
|
|
898c402dfc | ||
|
|
9258819c84 | ||
|
|
b5db0b511c | ||
|
|
7c6804449c | ||
|
|
ff28e2bf8f | ||
|
|
3510cd1be0 | ||
|
|
5320e8d9cc | ||
|
|
6ab9e2958d | ||
|
|
e47f286bda | ||
|
|
84b1599ca6 | ||
|
|
c5396a8ec8 | ||
|
|
9a9f18184b | ||
|
|
10d97e16e3 | ||
|
|
5ece500396 | ||
|
|
1a311513ca | ||
|
|
6e0e4835d1 | ||
|
|
ae4a784c3a | ||
|
|
287be65e7f | ||
|
|
476653a6cc | ||
|
|
407496efd6 | ||
|
|
9995b807f9 | ||
|
|
a6f4692b75 | ||
|
|
024b7ff824 | ||
|
|
68e46096ad | ||
|
|
d9f5f60854 | ||
|
|
82ccd4ddce | ||
|
|
5f69b337a6 | ||
|
|
dbbb6802d4 | ||
|
|
459cb44ff4 | ||
|
|
c1ebc813d5 | ||
|
|
948c42055b | ||
|
|
06d049d882 | ||
|
|
04f5e91e92 | ||
|
|
02a479ba13 | ||
|
|
01b99d20f6 | ||
|
|
9c6cd7f692 | ||
|
|
7899829dc7 | ||
|
|
d0693d362a | ||
|
|
227066f355 | ||
|
|
348c43b259 | ||
|
|
a23b7e1594 | ||
|
|
1c7952f8dd | ||
|
|
f0ef4c6ba6 | ||
|
|
eb71b640c5 | ||
|
|
6b7a0ff1ce | ||
|
|
d08740b6d7 | ||
|
|
70e3649e60 | ||
|
|
24d79f9b20 | ||
|
|
43c2ae6746 | ||
|
|
4bd2a3ec7f | ||
|
|
a90b375a72 | ||
|
|
9f1e562a19 | ||
|
|
4edfcff2e0 | ||
|
|
d4b48ab235 | ||
|
|
f1e5c31110 | ||
|
|
32c4522b47 | ||
|
|
fc760e1a84 | ||
|
|
2a92bf4dec | ||
|
|
716b253a04 | ||
|
|
0384365315 | ||
|
|
45dc910e01 | ||
|
|
f7e57e7d39 | ||
|
|
ace4151bbc | ||
|
|
088c3a35ed | ||
|
|
608bece8dc | ||
|
|
119caebfa8 | ||
|
|
2e0771b8d9 | ||
|
|
a08080a2ce | ||
|
|
d4bcdfde3e | ||
|
|
249ebfb590 | ||
|
|
56fc50e5ad | ||
|
|
5d8db36753 | ||
|
|
54f493467e | ||
|
|
7ed39db953 | ||
|
|
cc28d7520e | ||
|
|
109b307d0f | ||
|
|
178ecc2e4a | ||
|
|
ddfa7220d8 | ||
|
|
812523baef | ||
|
|
c5fd423a78 | ||
|
|
0b6c308d2e | ||
|
|
d6ef5e5404 | ||
|
|
9c304b8965 | ||
|
|
05c5a7fc30 | ||
|
|
2a2d4f09f1 | ||
|
|
ce5cd7bc8f | ||
|
|
a072a7b7d6 | ||
|
|
8a52f4fbbb | ||
|
|
ba07405d9a | ||
|
|
6239457f01 | ||
|
|
584fe4bd37 | ||
|
|
1f4fa2ed90 | ||
|
|
d24731999a | ||
|
|
1a3e82a1b0 | ||
|
|
f45b10f2ff | ||
|
|
2097fea6a0 | ||
|
|
c4bdb39c6d | ||
|
|
2f2b411293 | ||
|
|
860b979afb | ||
|
|
401d3f65f5 | ||
|
|
051cf043f3 | ||
|
|
4ebb4cfd9e | ||
|
|
d58a2d2717 | ||
|
|
51b764a39f | ||
|
|
0f94a05a69 | ||
|
|
16fd3c067f | ||
|
|
e13881763c | ||
|
|
90241c6076 | ||
|
|
d020a97209 | ||
|
|
d9ceea448f | ||
|
|
ec01846653 | ||
|
|
7d3c0a9d13 | ||
|
|
de88e3072b | ||
|
|
5e2ad8eb19 | ||
|
|
127107a96c | ||
|
|
1587c6facb | ||
|
|
042fadca28 | ||
|
|
4a9e6f6cc6 | ||
|
|
b7ac24eb7b | ||
|
|
c78d26a701 | ||
|
|
53614661c1 | ||
|
|
ea8c300a1a | ||
|
|
bb91bf3918 | ||
|
|
b9f72c293d | ||
|
|
7a14a394c2 | ||
|
|
dbf7148fbe | ||
|
|
59d51d1b15 | ||
|
|
acff4602cd | ||
|
|
44f135eb14 | ||
|
|
f4945c024e | ||
|
|
a329e2a3da | ||
|
|
608edf2569 | ||
|
|
f203e02a45 | ||
|
|
bdcc00af1f | ||
|
|
21d004fb26 | ||
|
|
d4ae307676 | ||
|
|
0a9993e85a | ||
|
|
1f2269dcf2 | ||
|
|
bb73916af0 | ||
|
|
12c4b7e924 | ||
|
|
c6b78b118c | ||
|
|
aaa6b6674a | ||
|
|
be2e173404 | ||
|
|
bfb189d55b | ||
|
|
3d503ed5ee | ||
|
|
ab01733860 | ||
|
|
dd0949b58d | ||
|
|
d825f74489 | ||
|
|
7f1ecdd585 | ||
|
|
01b2e205be | ||
|
|
d398e84f25 | ||
|
|
ac427ed1ec | ||
|
|
0118ccb614 | ||
|
|
697e059b66 | ||
|
|
45539018f5 | ||
|
|
1ebf2b6272 | ||
|
|
800dae280a | ||
|
|
bccdf269f0 | ||
|
|
757072cb01 | ||
|
|
b601d31d5c | ||
|
|
86ae501ce5 | ||
|
|
c22e5c146f | ||
|
|
4a710b4ffe | ||
|
|
f6ad3747f4 | ||
|
|
ec205bb7a2 | ||
|
|
464edddacf | ||
|
|
e6c6bce8a7 | ||
|
|
6a413b4d29 | ||
|
|
4f30e2e3c7 | ||
|
|
2bef94e3b4 | ||
|
|
cf94c417d2 | ||
|
|
e82100308c | ||
|
|
ada67a0f97 | ||
|
|
1bd20f7144 | ||
|
|
9b295ad85e | ||
|
|
d1953215e8 | ||
|
|
e480aca8b2 | ||
|
|
2befe20f76 | ||
|
|
c16485ad0b | ||
|
|
f036e99450 | ||
|
|
2804a36920 | ||
|
|
30e62813c7 | ||
|
|
621ce390c2 | ||
|
|
8d40423d9d | ||
|
|
1cf8959b92 | ||
|
|
56aafe3374 | ||
|
|
5570bd2999 | ||
|
|
9bec394436 | ||
|
|
3ae1a7ccfd | ||
|
|
e0b1c4c16f | ||
|
|
eb5bc560a4 | ||
|
|
f9309b7c54 | ||
|
|
3c2a154b39 | ||
|
|
23d07eb43e | ||
|
|
20af0edd4d | ||
|
|
4b29daabd4 | ||
|
|
a60513e998 | ||
|
|
188b96b951 | ||
|
|
b8e9dca6d3 | ||
|
|
157537c6ec | ||
|
|
a363b92075 | ||
|
|
f993d3d365 | ||
|
|
3f4520edf3 | ||
|
|
6e7041d8c5 | ||
|
|
e6cf5971a6 | ||
|
|
845af42080 | ||
|
|
c74481590b | ||
|
|
0ac5447198 | ||
|
|
e0428f8a91 | ||
|
|
cc0d2cb1d4 | ||
|
|
a0769eaf9a | ||
|
|
dc35545043 | ||
|
|
890709429c | ||
|
|
6f6a561b9e | ||
|
|
91d314c861 | ||
|
|
8abae9d4be | ||
|
|
26a390760e | ||
|
|
87933a2c92 | ||
|
|
1f843bb5d6 | ||
|
|
e87e78a372 | ||
|
|
91c82302bb | ||
|
|
d5a41dce1d | ||
|
|
40ab1d848c | ||
|
|
cc3a0a4144 | ||
|
|
5448403f43 | ||
|
|
636dd139c2 | ||
|
|
e7ad69be26 | ||
|
|
2f56b6dc3a | ||
|
|
ba7073df98 | ||
|
|
457bf6dfbb | ||
|
|
1e6279e1ca | ||
|
|
6e455d0eba | ||
|
|
9c9a74b448 | ||
|
|
b7790961fc | ||
|
|
5ecc050b12 | ||
|
|
c9819bde7f | ||
|
|
12660449ed | ||
|
|
7237e0affc | ||
|
|
85bb68825b | ||
|
|
48b8a28c7a |
@@ -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
|
||||
|
||||
3
.github/ISSUE_TEMPLATE/Bug-report.yml
vendored
3
.github/ISSUE_TEMPLATE/Bug-report.yml
vendored
@@ -12,6 +12,8 @@ body:
|
||||
required: true
|
||||
- label: I also read **[Setting-up](https://github.com/JustArchiNET/ArchiSteamFarm/wiki/Setting-up)** and **[FAQ](https://github.com/JustArchiNET/ArchiSteamFarm/wiki/FAQ)**, I don't need **[help](https://github.com/JustArchiNET/ArchiSteamFarm/blob/main/.github/SUPPORT.md)**, this is a bug report
|
||||
required: true
|
||||
- label: This is not a **[duplicate](https://github.com/JustArchiNET/ArchiSteamFarm/issues?q=is%3Aissue)** of an existing issue
|
||||
required: true
|
||||
- label: I don't own more than **[10 accounts in total](https://github.com/JustArchiNET/ArchiSteamFarm/wiki/FAQ#how-many-bots-can-i-run-with-asf)**
|
||||
required: true
|
||||
- label: I'm not using **[custom plugins](https://github.com/JustArchiNET/ArchiSteamFarm/wiki/Plugins)**
|
||||
@@ -147,6 +149,7 @@ body:
|
||||
|
||||
Ensure that your config has redacted (but NOT removed) potentially-sensitive properties, such as:
|
||||
- IPCPassword (recommended)
|
||||
- LicenseID (mandatory)
|
||||
- SteamOwnerID (optionally)
|
||||
- WebProxy (optionally, if exposing private details)
|
||||
- WebProxyPassword (optionally, if exposing private details)
|
||||
|
||||
2
.github/ISSUE_TEMPLATE/Enhancement-idea.yml
vendored
2
.github/ISSUE_TEMPLATE/Enhancement-idea.yml
vendored
@@ -12,6 +12,8 @@ body:
|
||||
required: true
|
||||
- label: I also read **[Setting-up](https://github.com/JustArchiNET/ArchiSteamFarm/wiki/Setting-up)** and **[FAQ](https://github.com/JustArchiNET/ArchiSteamFarm/wiki/FAQ)**, I don't need **[help](https://github.com/JustArchiNET/ArchiSteamFarm/blob/main/.github/SUPPORT.md)**, this is an enhancement idea
|
||||
required: true
|
||||
- label: This is not a **[duplicate](https://github.com/JustArchiNET/ArchiSteamFarm/issues?q=is%3Aissue)** of an existing issue
|
||||
required: true
|
||||
- label: My idea doesn't duplicate existing ASF functionality described on the **[wiki](https://github.com/JustArchiNET/ArchiSteamFarm/wiki)**
|
||||
required: true
|
||||
- label: I believe that my idea falls into ASF's scope and should be offered as part of ASF built-in functionality
|
||||
|
||||
2
.github/SUPPORT.md
vendored
2
.github/SUPPORT.md
vendored
@@ -2,6 +2,6 @@
|
||||
|
||||
Our **[wiki](https://github.com/JustArchiNET/ArchiSteamFarm/wiki)** is the official online documentation which covers at least a significant majority (if not all) of ASF subjects you could be interested in. We recommend to start with **[setting up](https://github.com/JustArchiNET/ArchiSteamFarm/wiki/Setting-up)**, **[configuration](https://github.com/JustArchiNET/ArchiSteamFarm/wiki/Configuration)** and our **[FAQ](https://github.com/JustArchiNET/ArchiSteamFarm/wiki/FAQ)** which should help you with setting up ASF, configuring it, as well as answering the most common questions that you might have. For more advanced matters, as well as further elaboration, we have other pages available on our **[wiki](https://github.com/JustArchiNET/ArchiSteamFarm/wiki)** that you can visit.
|
||||
|
||||
We also have three independent support channels dedicated to our ASF users, in case you couldn't manage to solve the issue yourself. We answer all support and technical matters in our **[GitHub discussions](https://github.com/JustArchiNET/ArchiSteamFarm/discussions/categories/support)**, **[Steam group](https://steamcommunity.com/groups/archiasf/discussions/1)**, and on our **[Discord server](https://discord.gg/hSQgt8j)**. You're free to use the support channel that matches your preferences, although keep in mind that you have a higher chance solving your issue on the GitHub or Steam, where we're doing our best to answer all questions that couldn't be answered by our community itself (as opposed to Discord server where we're not active 24/7 and therefore not always able to answer).
|
||||
We also have three independent support channels dedicated to our ASF users, in case you couldn't manage to solve the issue yourself. We answer all support and technical matters in our **[GitHub discussions](https://github.com/JustArchiNET/ArchiSteamFarm/discussions/categories/support-english)**, **[Steam group](https://steamcommunity.com/groups/archiasf/discussions/1)**, and on our **[Discord server](https://discord.gg/hSQgt8j)**. You're free to use the support channel that matches your preferences, although keep in mind that you have a higher chance solving your issue on the GitHub or Steam, where we're doing our best to answer all questions that couldn't be answered by our community itself (as opposed to Discord server where we're not active 24/7 and therefore not always able to answer).
|
||||
|
||||
GitHub **issues** (unlike discussions), are being used solely for ASF development, especially in regards to bugs and enhancements. We have a very strict policy regarding that, as GitHub issues is **not** a general support channel, it's dedicated exclusively to ASF development and we're not answering common ASF matters there, as we have appropriate support channels (mentioned above) for that. Common matters include not only general questions or issues that are obviously related to program usage, but also users reporting "bugs" that are clearly considered intended behaviour coming for example (and mainly) from misconfiguration or lack of understanding how the program works. If you're not sure whether your matter relates to ASF development or not, especially if you're not sure if it's a bug or intended behaviour, we recommend to use a support channel instead, where we'll answer you in calm atmosphere and forward your matter as GitHub issue if deemed appropriate. Invalid GitHub issues will be closed immediately and won't be answered.
|
||||
|
||||
2
.github/workflows/ci.yml
vendored
2
.github/workflows/ci.yml
vendored
@@ -42,7 +42,7 @@ jobs:
|
||||
|
||||
- name: Upload latest strings for translation on Crowdin
|
||||
if: ${{ github.event_name == 'push' && github.ref == 'refs/heads/main' && matrix.configuration == 'Release' && startsWith(matrix.os, 'ubuntu-') }}
|
||||
uses: crowdin/github-action@v1.15.2
|
||||
uses: crowdin/github-action@v1.19.0
|
||||
with:
|
||||
crowdin_branch_name: main
|
||||
config: '.github/crowdin.yml'
|
||||
|
||||
6
.github/workflows/code-quality.yml
vendored
6
.github/workflows/code-quality.yml
vendored
@@ -33,15 +33,13 @@ jobs:
|
||||
show-progress: false
|
||||
|
||||
- name: Run Qodana scan
|
||||
if: false # TODO: Enable GitHub results after qodana starts reporting meaningful stuff
|
||||
uses: JetBrains/qodana-action@v2023.2.9
|
||||
uses: JetBrains/qodana-action@v2023.3.1
|
||||
with:
|
||||
args: --property=idea.headless.enable.statistics=false
|
||||
env:
|
||||
QODANA_TOKEN: ${{ secrets.QODANA_TOKEN }}
|
||||
|
||||
- name: Report Qodana results to GitHub
|
||||
if: false # TODO: Enable GitHub results after qodana starts reporting meaningful stuff
|
||||
uses: github/codeql-action/upload-sarif@v2.22.8
|
||||
uses: github/codeql-action/upload-sarif@v3.24.8
|
||||
with:
|
||||
sarif_file: ${{ runner.temp }}/qodana/results/qodana.sarif.json
|
||||
|
||||
4
.github/workflows/docker-ci.yml
vendored
4
.github/workflows/docker-ci.yml
vendored
@@ -25,10 +25,10 @@ jobs:
|
||||
submodules: recursive
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v3.0.0
|
||||
uses: docker/setup-buildx-action@v3.2.0
|
||||
|
||||
- name: Build ${{ matrix.configuration }} Docker image from ${{ matrix.file }}
|
||||
uses: docker/build-push-action@v5.1.0
|
||||
uses: docker/build-push-action@v5.3.0
|
||||
with:
|
||||
context: .
|
||||
file: ${{ matrix.file }}
|
||||
|
||||
8
.github/workflows/docker-publish-latest.yml
vendored
8
.github/workflows/docker-publish-latest.yml
vendored
@@ -24,17 +24,17 @@ jobs:
|
||||
submodules: recursive
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v3.0.0
|
||||
uses: docker/setup-buildx-action@v3.2.0
|
||||
|
||||
- name: Login to ghcr.io
|
||||
uses: docker/login-action@v3.0.0
|
||||
uses: docker/login-action@v3.1.0
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ github.actor }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Login to DockerHub
|
||||
uses: docker/login-action@v3.0.0
|
||||
uses: docker/login-action@v3.1.0
|
||||
with:
|
||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||
@@ -59,7 +59,7 @@ jobs:
|
||||
echo "DH_REPOSITORY=$(echo ${{ secrets.DOCKERHUB_USERNAME }}/${{ github.event.repository.name }} | tr '[:upper:]' '[:lower:]')" >> "$GITHUB_ENV"
|
||||
|
||||
- name: Build and publish Docker image from Dockerfile.Service
|
||||
uses: docker/build-push-action@v5.1.0
|
||||
uses: docker/build-push-action@v5.3.0
|
||||
with:
|
||||
context: .
|
||||
file: Dockerfile.Service
|
||||
|
||||
10
.github/workflows/docker-publish-main.yml
vendored
10
.github/workflows/docker-publish-main.yml
vendored
@@ -25,17 +25,17 @@ jobs:
|
||||
submodules: recursive
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v3.0.0
|
||||
uses: docker/setup-buildx-action@v3.2.0
|
||||
|
||||
- name: Login to ghcr.io
|
||||
uses: docker/login-action@v3.0.0
|
||||
uses: docker/login-action@v3.1.0
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ github.actor }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Login to DockerHub
|
||||
uses: docker/login-action@v3.0.0
|
||||
uses: docker/login-action@v3.1.0
|
||||
with:
|
||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||
@@ -59,7 +59,7 @@ jobs:
|
||||
echo "DH_REPOSITORY=$(echo ${{ secrets.DOCKERHUB_USERNAME }}/${{ github.event.repository.name }} | tr '[:upper:]' '[:lower:]')" >> "$GITHUB_ENV"
|
||||
|
||||
- name: Build and publish Docker image from Dockerfile
|
||||
uses: docker/build-push-action@v5.1.0
|
||||
uses: docker/build-push-action@v5.3.0
|
||||
with:
|
||||
context: .
|
||||
platforms: ${{ env.PLATFORMS }}
|
||||
@@ -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 }}
|
||||
|
||||
@@ -25,17 +25,17 @@ jobs:
|
||||
submodules: recursive
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v3.0.0
|
||||
uses: docker/setup-buildx-action@v3.2.0
|
||||
|
||||
- name: Login to ghcr.io
|
||||
uses: docker/login-action@v3.0.0
|
||||
uses: docker/login-action@v3.1.0
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ github.actor }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Login to DockerHub
|
||||
uses: docker/login-action@v3.0.0
|
||||
uses: docker/login-action@v3.1.0
|
||||
with:
|
||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||
@@ -60,7 +60,7 @@ jobs:
|
||||
echo "DH_REPOSITORY=$(echo ${{ secrets.DOCKERHUB_USERNAME }}/${{ github.event.repository.name }} | tr '[:upper:]' '[:lower:]')" >> "$GITHUB_ENV"
|
||||
|
||||
- name: Build and publish Docker image from Dockerfile
|
||||
uses: docker/build-push-action@v5.1.0
|
||||
uses: docker/build-push-action@v5.3.0
|
||||
with:
|
||||
context: .
|
||||
platforms: ${{ env.PLATFORMS }}
|
||||
|
||||
44
.github/workflows/publish.yml
vendored
44
.github/workflows/publish.yml
vendored
@@ -24,7 +24,7 @@ jobs:
|
||||
submodules: recursive
|
||||
|
||||
- name: Setup Node.js with npm
|
||||
uses: actions/setup-node@v4.0.0
|
||||
uses: actions/setup-node@v4.0.2
|
||||
with:
|
||||
check-latest: true
|
||||
node-version: ${{ env.NODE_JS_VERSION }}
|
||||
@@ -42,7 +42,7 @@ jobs:
|
||||
run: npm run-script deploy --no-progress --prefix ASF-ui
|
||||
|
||||
- name: Upload ASF-ui
|
||||
uses: actions/upload-artifact@v3.1.3
|
||||
uses: actions/upload-artifact@v4.3.1
|
||||
with:
|
||||
name: ASF-ui
|
||||
path: ASF-ui/dist
|
||||
@@ -88,7 +88,7 @@ jobs:
|
||||
run: dotnet --info
|
||||
|
||||
- name: Download previously built ASF-ui
|
||||
uses: actions/download-artifact@v3.0.2
|
||||
uses: actions/download-artifact@v4.1.4
|
||||
with:
|
||||
name: ASF-ui
|
||||
path: ASF-ui/dist
|
||||
@@ -143,8 +143,17 @@ jobs:
|
||||
$ProgressPreference = 'SilentlyContinue'
|
||||
|
||||
dotnet restore
|
||||
|
||||
if ($LastExitCode -ne 0) {
|
||||
throw "Last command failed."
|
||||
}
|
||||
|
||||
dotnet build ArchiSteamFarm -c "$env:CONFIGURATION" -p:ContinuousIntegrationBuild=true -p:TargetLatestRuntimePatch=false -p:UseAppHost=false --nologo
|
||||
|
||||
if ($LastExitCode -ne 0) {
|
||||
throw "Last command failed."
|
||||
}
|
||||
|
||||
- name: Prepare ArchiSteamFarm.OfficialPlugins.SteamTokenDumper on Unix
|
||||
if: startsWith(matrix.os, 'macos-') || startsWith(matrix.os, 'ubuntu-')
|
||||
env:
|
||||
@@ -396,7 +405,7 @@ jobs:
|
||||
}
|
||||
|
||||
- name: Upload ASF-${{ matrix.variant }}
|
||||
uses: actions/upload-artifact@v3.1.3
|
||||
uses: actions/upload-artifact@v4.3.1
|
||||
with:
|
||||
name: ${{ matrix.os }}_ASF-${{ matrix.variant }}
|
||||
path: out/ASF-${{ matrix.variant }}.zip
|
||||
@@ -416,55 +425,55 @@ jobs:
|
||||
show-progress: false
|
||||
|
||||
- name: Download ASF-generic artifact from ubuntu-latest
|
||||
uses: actions/download-artifact@v3.0.2
|
||||
uses: actions/download-artifact@v4.1.4
|
||||
with:
|
||||
name: ubuntu-latest_ASF-generic
|
||||
path: out
|
||||
|
||||
- name: Download ASF-linux-arm artifact from ubuntu-latest
|
||||
uses: actions/download-artifact@v3.0.2
|
||||
uses: actions/download-artifact@v4.1.4
|
||||
with:
|
||||
name: ubuntu-latest_ASF-linux-arm
|
||||
path: out
|
||||
|
||||
- name: Download ASF-linux-arm64 artifact from ubuntu-latest
|
||||
uses: actions/download-artifact@v3.0.2
|
||||
uses: actions/download-artifact@v4.1.4
|
||||
with:
|
||||
name: ubuntu-latest_ASF-linux-arm64
|
||||
path: out
|
||||
|
||||
- name: Download ASF-linux-x64 artifact from ubuntu-latest
|
||||
uses: actions/download-artifact@v3.0.2
|
||||
uses: actions/download-artifact@v4.1.4
|
||||
with:
|
||||
name: ubuntu-latest_ASF-linux-x64
|
||||
path: out
|
||||
|
||||
- name: Download ASF-osx-arm64 artifact from macos-latest
|
||||
uses: actions/download-artifact@v3.0.2
|
||||
uses: actions/download-artifact@v4.1.4
|
||||
with:
|
||||
name: macos-latest_ASF-osx-arm64
|
||||
path: out
|
||||
|
||||
- name: Download ASF-osx-x64 artifact from macos-latest
|
||||
uses: actions/download-artifact@v3.0.2
|
||||
uses: actions/download-artifact@v4.1.4
|
||||
with:
|
||||
name: macos-latest_ASF-osx-x64
|
||||
path: out
|
||||
|
||||
- name: Download ASF-win-arm64 artifact from windows-latest
|
||||
uses: actions/download-artifact@v3.0.2
|
||||
uses: actions/download-artifact@v4.1.4
|
||||
with:
|
||||
name: windows-latest_ASF-win-arm64
|
||||
path: out
|
||||
|
||||
- name: Download ASF-win-x64 artifact from windows-latest
|
||||
uses: actions/download-artifact@v3.0.2
|
||||
uses: actions/download-artifact@v4.1.4
|
||||
with:
|
||||
name: windows-latest_ASF-win-x64
|
||||
path: out
|
||||
|
||||
- name: Import GPG key for signing
|
||||
uses: crazy-max/ghaction-import-gpg@v6.0.0
|
||||
uses: crazy-max/ghaction-import-gpg@v6.1.0
|
||||
with:
|
||||
gpg_private_key: ${{ secrets.ARCHIBOT_GPG_PRIVATE_KEY }}
|
||||
|
||||
@@ -481,23 +490,26 @@ jobs:
|
||||
)
|
||||
|
||||
- name: Upload SHA512SUMS
|
||||
uses: actions/upload-artifact@v3.1.3
|
||||
uses: actions/upload-artifact@v4.3.1
|
||||
with:
|
||||
name: SHA512SUMS
|
||||
path: out/SHA512SUMS
|
||||
|
||||
- name: Upload SHA512SUMS.sign
|
||||
uses: actions/upload-artifact@v3.1.3
|
||||
uses: actions/upload-artifact@v4.3.1
|
||||
with:
|
||||
name: SHA512SUMS.sign
|
||||
path: out/SHA512SUMS.sign
|
||||
|
||||
- name: Create ArchiSteamFarm GitHub release
|
||||
uses: ncipollo/release-action@v1.13.0
|
||||
uses: ncipollo/release-action@v1.14.0
|
||||
with:
|
||||
allowUpdates: true
|
||||
artifactErrorsFailBuild: true
|
||||
artifacts: "out/*"
|
||||
bodyFile: .github/RELEASE_TEMPLATE.md
|
||||
makeLatest: false
|
||||
name: ArchiSteamFarm V${{ github.ref_name }}
|
||||
prerelease: true
|
||||
token: ${{ secrets.ARCHIBOT_GITHUB_TOKEN }}
|
||||
updateOnlyUnreleased: true
|
||||
|
||||
4
.github/workflows/translations.yml
vendored
4
.github/workflows/translations.yml
vendored
@@ -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
2
ASF-ui
Submodule ASF-ui updated: 62a5d46a03...25ce8efeb9
@@ -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>
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
// ----------------------------------------------------------------------------------------------
|
||||
// _ _ _ ____ _ _____
|
||||
// / \ _ __ ___ | |__ (_)/ ___| | |_ ___ __ _ _ __ ___ | ___|__ _ _ __ _ __ ___
|
||||
// / _ \ | '__|/ __|| '_ \ | |\___ \ | __|/ _ \ / _` || '_ ` _ \ | |_ / _` || '__|| '_ ` _ \
|
||||
// / ___ \ | | | (__ | | | || | ___) || |_| __/| (_| || | | | | || _|| (_| || | | | | | | |
|
||||
// /_/ \_\|_| \___||_| |_||_||____/ \__|\___| \__,_||_| |_| |_||_| \__,_||_| |_| |_| |_|
|
||||
// ----------------------------------------------------------------------------------------------
|
||||
// |
|
||||
// Copyright 2015-2023 Łukasz "JustArchi" Domeradzki
|
||||
// Copyright 2015-2024 Łukasz "JustArchi" Domeradzki
|
||||
// Contact: JustArchi@JustArchi.net
|
||||
// |
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
// ----------------------------------------------------------------------------------------------
|
||||
// _ _ _ ____ _ _____
|
||||
// / \ _ __ ___ | |__ (_)/ ___| | |_ ___ __ _ _ __ ___ | ___|__ _ _ __ _ __ ___
|
||||
// / _ \ | '__|/ __|| '_ \ | |\___ \ | __|/ _ \ / _` || '_ ` _ \ | |_ / _` || '__|| '_ ` _ \
|
||||
// / ___ \ | | | (__ | | | || | ___) || |_| __/| (_| || | | | | || _|| (_| || | | | | | | |
|
||||
// /_/ \_\|_| \___||_| |_||_||____/ \__|\___| \__,_||_| |_| |_||_| \__,_||_| |_| |_| |_|
|
||||
// ----------------------------------------------------------------------------------------------
|
||||
// |
|
||||
// Copyright 2015-2023 Łukasz "JustArchi" Domeradzki
|
||||
// Copyright 2015-2024 Łukasz "JustArchi" Domeradzki
|
||||
// Contact: JustArchi@JustArchi.net
|
||||
// |
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
// ----------------------------------------------------------------------------------------------
|
||||
// _ _ _ ____ _ _____
|
||||
// / \ _ __ ___ | |__ (_)/ ___| | |_ ___ __ _ _ __ ___ | ___|__ _ _ __ _ __ ___
|
||||
// / _ \ | '__|/ __|| '_ \ | |\___ \ | __|/ _ \ / _` || '_ ` _ \ | |_ / _` || '__|| '_ ` _ \
|
||||
// / ___ \ | | | (__ | | | || | ___) || |_| __/| (_| || | | | | || _|| (_| || | | | | | | |
|
||||
// /_/ \_\|_| \___||_| |_||_||____/ \__|\___| \__,_||_| |_| |_||_| \__,_||_| |_| |_| |_|
|
||||
// ----------------------------------------------------------------------------------------------
|
||||
// |
|
||||
// Copyright 2015-2023 Łukasz "JustArchi" Domeradzki
|
||||
// Copyright 2015-2024 Łukasz "JustArchi" Domeradzki
|
||||
// Contact: JustArchi@JustArchi.net
|
||||
// |
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
// ----------------------------------------------------------------------------------------------
|
||||
// _ _ _ ____ _ _____
|
||||
// / \ _ __ ___ | |__ (_)/ ___| | |_ ___ __ _ _ __ ___ | ___|__ _ _ __ _ __ ___
|
||||
// / _ \ | '__|/ __|| '_ \ | |\___ \ | __|/ _ \ / _` || '_ ` _ \ | |_ / _` || '__|| '_ ` _ \
|
||||
// / ___ \ | | | (__ | | | || | ___) || |_| __/| (_| || | | | | || _|| (_| || | | | | | | |
|
||||
// /_/ \_\|_| \___||_| |_||_||____/ \__|\___| \__,_||_| |_| |_||_| \__,_||_| |_| |_| |_|
|
||||
// ----------------------------------------------------------------------------------------------
|
||||
// |
|
||||
// Copyright 2015-2023 Łukasz "JustArchi" Domeradzki
|
||||
// Copyright 2015-2024 Łukasz "JustArchi" Domeradzki
|
||||
// Contact: JustArchi@JustArchi.net
|
||||
// |
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
@@ -21,16 +23,17 @@
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using System.Composition;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.Linq;
|
||||
using System.Text.Json;
|
||||
using System.Text.Json.Serialization;
|
||||
using System.Threading.Tasks;
|
||||
using ArchiSteamFarm.Core;
|
||||
using ArchiSteamFarm.Plugins.Interfaces;
|
||||
using ArchiSteamFarm.Steam;
|
||||
using ArchiSteamFarm.Steam.Data;
|
||||
using Newtonsoft.Json;
|
||||
using Newtonsoft.Json.Linq;
|
||||
using SteamKit2;
|
||||
|
||||
namespace ArchiSteamFarm.CustomPlugins.ExamplePlugin;
|
||||
@@ -45,31 +48,35 @@ namespace ArchiSteamFarm.CustomPlugins.ExamplePlugin;
|
||||
internal sealed class ExamplePlugin : IASF, IBot, IBotCommand2, IBotConnection, IBotFriendRequest, IBotMessage, IBotModules, IBotTradeOffer {
|
||||
// This is used for identification purposes, typically you want to use a friendly name of your plugin here, such as the name of your main class
|
||||
// Please note that this property can have direct dependencies only on structures that were initialized by the constructor, as it's possible to be called before OnLoaded() takes place
|
||||
[JsonInclude]
|
||||
[Required]
|
||||
public string Name => nameof(ExamplePlugin);
|
||||
|
||||
// This will be displayed to the user and written in the log file, typically you should point it to the version of your library, but alternatively you can do some more advanced logic if you'd like to
|
||||
// Please note that this property can have direct dependencies only on structures that were initialized by the constructor, as it's possible to be called before OnLoaded() takes place
|
||||
[JsonInclude]
|
||||
[Required]
|
||||
public Version Version => typeof(ExamplePlugin).Assembly.GetName().Version ?? throw new InvalidOperationException(nameof(Version));
|
||||
|
||||
// Plugins can expose custom properties for our GET /Api/Plugins API call, simply annotate them with [JsonProperty] (or keep public)
|
||||
[JsonProperty]
|
||||
public bool CustomIsEnabledField { get; private set; } = true;
|
||||
[JsonInclude]
|
||||
[Required]
|
||||
public bool CustomIsEnabledField { get; private init; } = true;
|
||||
|
||||
// This method, apart from being called before any bot initialization takes place, allows you to read custom global config properties that are not recognized by ASF
|
||||
// Thanks to that, you can extend default ASF config with your own stuff, then parse it here in order to customize your plugin during runtime
|
||||
// Keep in mind that, as noted in the interface, additionalConfigProperties can be null if no custom, unrecognized properties are found by ASF, you should handle that case appropriately
|
||||
// In addition to that, this method also guarantees that all plugins were already OnLoaded(), which allows cross-plugins-communication to be possible
|
||||
public Task OnASFInit(IReadOnlyDictionary<string, JToken>? additionalConfigProperties = null) {
|
||||
public Task OnASFInit(IReadOnlyDictionary<string, JsonElement>? additionalConfigProperties = null) {
|
||||
if (additionalConfigProperties == null) {
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
foreach ((string configProperty, JToken configValue) in additionalConfigProperties) {
|
||||
foreach ((string configProperty, JsonElement configValue) in additionalConfigProperties) {
|
||||
// It's a good idea to prefix your custom properties with the name of your plugin, so there will be no possible conflict of ASF or other plugins using the same name, neither now or in the future
|
||||
switch (configProperty) {
|
||||
case $"{nameof(ExamplePlugin)}TestProperty" when configValue.Type == JTokenType.Boolean:
|
||||
bool exampleBooleanValue = configValue.Value<bool>();
|
||||
ASF.ArchiLogger.LogGenericInfo($"{nameof(ExamplePlugin)}TestProperty boolean property has been found with a value of: {exampleBooleanValue}");
|
||||
case $"{nameof(ExamplePlugin)}TestProperty" when configValue.ValueKind == JsonValueKind.True:
|
||||
ASF.ArchiLogger.LogGenericInfo($"{nameof(ExamplePlugin)}TestProperty boolean property has been found with a value of true");
|
||||
|
||||
break;
|
||||
}
|
||||
@@ -135,7 +142,7 @@ internal sealed class ExamplePlugin : IASF, IBot, IBotCommand2, IBotConnection,
|
||||
// Keep in mind that, as noted in the interface, additionalConfigProperties can be null if no custom, unrecognized properties are found by ASF, you should handle that case appropriately
|
||||
// Also keep in mind that this function can be called multiple times, e.g. when user edits their bot configs during runtime
|
||||
// Take a look at OnASFInit() for example parsing code
|
||||
public async Task OnBotInitModules(Bot bot, IReadOnlyDictionary<string, JToken>? additionalConfigProperties = null) {
|
||||
public async Task OnBotInitModules(Bot bot, IReadOnlyDictionary<string, JsonElement>? additionalConfigProperties = null) {
|
||||
// For example, we'll ensure that every bot starts paused regardless of Paused property, in order to do this, we'll just call Pause here in InitModules()
|
||||
// Thanks to the fact that this method is called with each bot config reload, we'll ensure that our bot stays paused even if it'd get unpaused otherwise
|
||||
bot.ArchiLogger.LogGenericInfo("Pausing this bot as asked from the plugin");
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
// ----------------------------------------------------------------------------------------------
|
||||
// _ _ _ ____ _ _____
|
||||
// / \ _ __ ___ | |__ (_)/ ___| | |_ ___ __ _ _ __ ___ | ___|__ _ _ __ _ __ ___
|
||||
// / _ \ | '__|/ __|| '_ \ | |\___ \ | __|/ _ \ / _` || '_ ` _ \ | |_ / _` || '__|| '_ ` _ \
|
||||
// / ___ \ | | | (__ | | | || | ___) || |_| __/| (_| || | | | | || _|| (_| || | | | | | | |
|
||||
// /_/ \_\|_| \___||_| |_||_||____/ \__|\___| \__,_||_| |_| |_||_| \__,_||_| |_| |_| |_|
|
||||
// ----------------------------------------------------------------------------------------------
|
||||
// |
|
||||
// Copyright 2015-2023 Łukasz "JustArchi" Domeradzki
|
||||
// Copyright 2015-2024 Łukasz "JustArchi" Domeradzki
|
||||
// Contact: JustArchi@JustArchi.net
|
||||
// |
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
@@ -21,15 +23,17 @@
|
||||
|
||||
using System;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using Newtonsoft.Json;
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace ArchiSteamFarm.CustomPlugins.ExamplePlugin;
|
||||
|
||||
#pragma warning disable CA1812 // False positive, the class is used during json deserialization
|
||||
[SuppressMessage("ReSharper", "ClassCannotBeInstantiated")]
|
||||
internal sealed class MeowResponse {
|
||||
[JsonProperty("url", Required = Required.Always)]
|
||||
internal readonly Uri URL = null!;
|
||||
[JsonInclude]
|
||||
[JsonPropertyName("url")]
|
||||
[JsonRequired]
|
||||
internal Uri URL { get; private init; } = null!;
|
||||
|
||||
[JsonConstructor]
|
||||
private MeowResponse() { }
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
// ----------------------------------------------------------------------------------------------
|
||||
// _ _ _ ____ _ _____
|
||||
// / \ _ __ ___ | |__ (_)/ ___| | |_ ___ __ _ _ __ ___ | ___|__ _ _ __ _ __ ___
|
||||
// / _ \ | '__|/ __|| '_ \ | |\___ \ | __|/ _ \ / _` || '_ ` _ \ | |_ / _` || '__|| '_ ` _ \
|
||||
// / ___ \ | | | (__ | | | || | ___) || |_| __/| (_| || | | | | || _|| (_| || | | | | | | |
|
||||
// /_/ \_\|_| \___||_| |_||_||____/ \__|\___| \__,_||_| |_| |_||_| \__,_||_| |_| |_| |_|
|
||||
// ----------------------------------------------------------------------------------------------
|
||||
// |
|
||||
// Copyright 2015-2023 Łukasz "JustArchi" Domeradzki
|
||||
// Copyright 2015-2024 Łukasz "JustArchi" Domeradzki
|
||||
// Contact: JustArchi@JustArchi.net
|
||||
// |
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
// ----------------------------------------------------------------------------------------------
|
||||
// _ _ _ ____ _ _____
|
||||
// / \ _ __ ___ | |__ (_)/ ___| | |_ ___ __ _ _ __ ___ | ___|__ _ _ __ _ __ ___
|
||||
// / _ \ | '__|/ __|| '_ \ | |\___ \ | __|/ _ \ / _` || '_ ` _ \ | |_ / _` || '__|| '_ ` _ \
|
||||
// / ___ \ | | | (__ | | | || | ___) || |_| __/| (_| || | | | | || _|| (_| || | | | | | | |
|
||||
// /_/ \_\|_| \___||_| |_||_||____/ \__|\___| \__,_||_| |_| |_||_| \__,_||_| |_| |_| |_|
|
||||
// ----------------------------------------------------------------------------------------------
|
||||
// |
|
||||
// Copyright 2015-2023 Łukasz "JustArchi" Domeradzki
|
||||
// Copyright 2015-2024 Łukasz "JustArchi" Domeradzki
|
||||
// Contact: JustArchi@JustArchi.net
|
||||
// |
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
@@ -20,8 +22,10 @@
|
||||
// limitations under the License.
|
||||
|
||||
using System;
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using System.Composition;
|
||||
using System.Runtime;
|
||||
using System.Text.Json.Serialization;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using ArchiSteamFarm.Core;
|
||||
@@ -38,8 +42,12 @@ internal sealed class PeriodicGCPlugin : IPlugin {
|
||||
private static readonly object LockObject = new();
|
||||
private static readonly Timer PeriodicGCTimer = new(PerformGC);
|
||||
|
||||
[JsonInclude]
|
||||
[Required]
|
||||
public string Name => nameof(PeriodicGCPlugin);
|
||||
|
||||
[JsonInclude]
|
||||
[Required]
|
||||
public Version Version => typeof(PeriodicGCPlugin).Assembly.GetName().Version ?? throw new InvalidOperationException(nameof(Version));
|
||||
|
||||
public Task OnLoaded() {
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
@@ -1,18 +1,20 @@
|
||||
// ----------------------------------------------------------------------------------------------
|
||||
// _ _ _ ____ _ _____
|
||||
// / \ _ __ ___ | |__ (_)/ ___| | |_ ___ __ _ _ __ ___ | ___|__ _ _ __ _ __ ___
|
||||
// / _ \ | '__|/ __|| '_ \ | |\___ \ | __|/ _ \ / _` || '_ ` _ \ | |_ / _` || '__|| '_ ` _ \
|
||||
// / ___ \ | | | (__ | | | || | ___) || |_| __/| (_| || | | | | || _|| (_| || | | | | | | |
|
||||
// /_/ \_\|_| \___||_| |_||_||____/ \__|\___| \__,_||_| |_| |_||_| \__,_||_| |_| |_| |_|
|
||||
// |
|
||||
// Copyright 2015-2023 Łukasz "JustArchi" Domeradzki
|
||||
// ----------------------------------------------------------------------------------------------
|
||||
//
|
||||
// Copyright 2015-2024 Łukasz "JustArchi" Domeradzki
|
||||
// Contact: JustArchi@JustArchi.net
|
||||
// |
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
// |
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
// |
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
|
||||
@@ -1,18 +1,20 @@
|
||||
// ----------------------------------------------------------------------------------------------
|
||||
// _ _ _ ____ _ _____
|
||||
// / \ _ __ ___ | |__ (_)/ ___| | |_ ___ __ _ _ __ ___ | ___|__ _ _ __ _ __ ___
|
||||
// / _ \ | '__|/ __|| '_ \ | |\___ \ | __|/ _ \ / _` || '_ ` _ \ | |_ / _` || '__|| '_ ` _ \
|
||||
// / ___ \ | | | (__ | | | || | ___) || |_| __/| (_| || | | | | || _|| (_| || | | | | | | |
|
||||
// /_/ \_\|_| \___||_| |_||_||____/ \__|\___| \__,_||_| |_| |_||_| \__,_||_| |_| |_| |_|
|
||||
// |
|
||||
// Copyright 2015-2023 Łukasz "JustArchi" Domeradzki
|
||||
// ----------------------------------------------------------------------------------------------
|
||||
//
|
||||
// Copyright 2015-2024 Łukasz "JustArchi" Domeradzki
|
||||
// Contact: JustArchi@JustArchi.net
|
||||
// |
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
// |
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
// |
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
@@ -20,11 +22,12 @@
|
||||
// limitations under the License.
|
||||
|
||||
using System;
|
||||
using Newtonsoft.Json;
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace ArchiSteamFarm.CustomPlugins.SignInWithSteam.Data;
|
||||
|
||||
public sealed class SignInWithSteamRequest {
|
||||
[JsonProperty(Required = Required.Always)]
|
||||
public Uri RedirectURL { get; private set; } = null!;
|
||||
[JsonInclude]
|
||||
[JsonRequired]
|
||||
public Uri RedirectURL { get; private init; } = null!;
|
||||
}
|
||||
|
||||
@@ -1,18 +1,20 @@
|
||||
// ----------------------------------------------------------------------------------------------
|
||||
// _ _ _ ____ _ _____
|
||||
// / \ _ __ ___ | |__ (_)/ ___| | |_ ___ __ _ _ __ ___ | ___|__ _ _ __ _ __ ___
|
||||
// / _ \ | '__|/ __|| '_ \ | |\___ \ | __|/ _ \ / _` || '_ ` _ \ | |_ / _` || '__|| '_ ` _ \
|
||||
// / ___ \ | | | (__ | | | || | ___) || |_| __/| (_| || | | | | || _|| (_| || | | | | | | |
|
||||
// /_/ \_\|_| \___||_| |_||_||____/ \__|\___| \__,_||_| |_| |_||_| \__,_||_| |_| |_| |_|
|
||||
// |
|
||||
// Copyright 2015-2023 Łukasz "JustArchi" Domeradzki
|
||||
// ----------------------------------------------------------------------------------------------
|
||||
//
|
||||
// Copyright 2015-2024 Łukasz "JustArchi" Domeradzki
|
||||
// Contact: JustArchi@JustArchi.net
|
||||
// |
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
// |
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
// |
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
@@ -20,13 +22,14 @@
|
||||
// limitations under the License.
|
||||
|
||||
using System;
|
||||
using Newtonsoft.Json;
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace ArchiSteamFarm.CustomPlugins.SignInWithSteam.Data;
|
||||
|
||||
public sealed class SignInWithSteamResponse {
|
||||
[JsonProperty(Required = Required.Always)]
|
||||
public Uri ReturnURL { get; private set; }
|
||||
[JsonInclude]
|
||||
[JsonRequired]
|
||||
public Uri ReturnURL { get; private init; }
|
||||
|
||||
internal SignInWithSteamResponse(Uri returnURL) {
|
||||
ArgumentNullException.ThrowIfNull(returnURL);
|
||||
|
||||
@@ -1,18 +1,20 @@
|
||||
// ----------------------------------------------------------------------------------------------
|
||||
// _ _ _ ____ _ _____
|
||||
// / \ _ __ ___ | |__ (_)/ ___| | |_ ___ __ _ _ __ ___ | ___|__ _ _ __ _ __ ___
|
||||
// / _ \ | '__|/ __|| '_ \ | |\___ \ | __|/ _ \ / _` || '_ ` _ \ | |_ / _` || '__|| '_ ` _ \
|
||||
// / ___ \ | | | (__ | | | || | ___) || |_| __/| (_| || | | | | || _|| (_| || | | | | | | |
|
||||
// /_/ \_\|_| \___||_| |_||_||____/ \__|\___| \__,_||_| |_| |_||_| \__,_||_| |_| |_| |_|
|
||||
// |
|
||||
// Copyright 2015-2023 Łukasz "JustArchi" Domeradzki
|
||||
// ----------------------------------------------------------------------------------------------
|
||||
//
|
||||
// Copyright 2015-2024 Łukasz "JustArchi" Domeradzki
|
||||
// Contact: JustArchi@JustArchi.net
|
||||
// |
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
// |
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
// |
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
|
||||
@@ -1,18 +1,20 @@
|
||||
// ----------------------------------------------------------------------------------------------
|
||||
// _ _ _ ____ _ _____
|
||||
// / \ _ __ ___ | |__ (_)/ ___| | |_ ___ __ _ _ __ ___ | ___|__ _ _ __ _ __ ___
|
||||
// / _ \ | '__|/ __|| '_ \ | |\___ \ | __|/ _ \ / _` || '_ ` _ \ | |_ / _` || '__|| '_ ` _ \
|
||||
// / ___ \ | | | (__ | | | || | ___) || |_| __/| (_| || | | | | || _|| (_| || | | | | | | |
|
||||
// /_/ \_\|_| \___||_| |_||_||____/ \__|\___| \__,_||_| |_| |_||_| \__,_||_| |_| |_| |_|
|
||||
// |
|
||||
// Copyright 2015-2023 Łukasz "JustArchi" Domeradzki
|
||||
// ----------------------------------------------------------------------------------------------
|
||||
//
|
||||
// Copyright 2015-2024 Łukasz "JustArchi" Domeradzki
|
||||
// Contact: JustArchi@JustArchi.net
|
||||
// |
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
// |
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
// |
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
@@ -20,7 +22,9 @@
|
||||
// limitations under the License.
|
||||
|
||||
using System;
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using System.Composition;
|
||||
using System.Text.Json.Serialization;
|
||||
using System.Threading.Tasks;
|
||||
using ArchiSteamFarm.Core;
|
||||
using ArchiSteamFarm.Plugins.Interfaces;
|
||||
@@ -31,8 +35,12 @@ namespace ArchiSteamFarm.CustomPlugins.SignInWithSteam;
|
||||
[Export(typeof(IPlugin))]
|
||||
[UsedImplicitly]
|
||||
internal sealed class SignInWithSteamPlugin : IPlugin {
|
||||
[JsonInclude]
|
||||
[Required]
|
||||
public string Name => nameof(SignInWithSteamPlugin);
|
||||
|
||||
[JsonInclude]
|
||||
[Required]
|
||||
public Version Version => typeof(SignInWithSteamPlugin).Assembly.GetName().Version ?? throw new InvalidOperationException(nameof(Version));
|
||||
|
||||
public Task OnLoaded() {
|
||||
|
||||
@@ -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" />
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
// ----------------------------------------------------------------------------------------------
|
||||
// _ _ _ ____ _ _____
|
||||
// / \ _ __ ___ | |__ (_)/ ___| | |_ ___ __ _ _ __ ___ | ___|__ _ _ __ _ __ ___
|
||||
// / _ \ | '__|/ __|| '_ \ | |\___ \ | __|/ _ \ / _` || '_ ` _ \ | |_ / _` || '__|| '_ ` _ \
|
||||
// / ___ \ | | | (__ | | | || | ___) || |_| __/| (_| || | | | | || _|| (_| || | | | | | | |
|
||||
// /_/ \_\|_| \___||_| |_||_||____/ \__|\___| \__,_||_| |_| |_||_| \__,_||_| |_| |_| |_|
|
||||
// ----------------------------------------------------------------------------------------------
|
||||
// |
|
||||
// Copyright 2015-2023 Łukasz "JustArchi" Domeradzki
|
||||
// Copyright 2015-2024 Łukasz "JustArchi" Domeradzki
|
||||
// Contact: JustArchi@JustArchi.net
|
||||
// |
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
// ----------------------------------------------------------------------------------------------
|
||||
// _ _ _ ____ _ _____
|
||||
// / \ _ __ ___ | |__ (_)/ ___| | |_ ___ __ _ _ __ ___ | ___|__ _ _ __ _ __ ___
|
||||
// / _ \ | '__|/ __|| '_ \ | |\___ \ | __|/ _ \ / _` || '_ ` _ \ | |_ / _` || '__|| '_ ` _ \
|
||||
// / ___ \ | | | (__ | | | || | ___) || |_| __/| (_| || | | | | || _|| (_| || | | | | | | |
|
||||
// /_/ \_\|_| \___||_| |_||_||____/ \__|\___| \__,_||_| |_| |_||_| \__,_||_| |_| |_| |_|
|
||||
// ----------------------------------------------------------------------------------------------
|
||||
// |
|
||||
// Copyright 2015-2023 Łukasz "JustArchi" Domeradzki
|
||||
// Copyright 2015-2024 Łukasz "JustArchi" Domeradzki
|
||||
// Contact: JustArchi@JustArchi.net
|
||||
// |
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
@@ -41,7 +43,7 @@ using SteamKit2;
|
||||
namespace ArchiSteamFarm.OfficialPlugins.ItemsMatcher;
|
||||
|
||||
internal static class Backend {
|
||||
internal static async Task<ObjectResponse<GenericResponse<BackgroundTaskResponse>>?> AnnounceDiffForListing(WebBrowser webBrowser, ulong steamID, IReadOnlyCollection<AssetForListing> inventory, string inventoryChecksum, IReadOnlyCollection<Asset.EType> acceptedMatchableTypes, uint totalInventoryCount, bool matchEverything, string tradeToken, IReadOnlyCollection<AssetForListing> inventoryRemoved, string? previousInventoryChecksum, string? nickname = null, string? avatarHash = null) {
|
||||
internal static async Task<ObjectResponse<GenericResponse<BackgroundTaskResponse>>?> AnnounceDiffForListing(WebBrowser webBrowser, ulong steamID, IReadOnlyCollection<AssetForListing> inventory, string inventoryChecksum, IReadOnlyCollection<EAssetType> acceptedMatchableTypes, uint totalInventoryCount, bool matchEverything, string tradeToken, IReadOnlyCollection<AssetForListing> inventoryRemoved, string? previousInventoryChecksum, string? nickname = null, string? avatarHash = null) {
|
||||
ArgumentNullException.ThrowIfNull(webBrowser);
|
||||
|
||||
if ((steamID == 0) || !new SteamID(steamID).IsIndividualAccount) {
|
||||
@@ -72,7 +74,7 @@ internal static class Backend {
|
||||
return await webBrowser.UrlPostToJsonObject<GenericResponse<BackgroundTaskResponse>, AnnouncementDiffRequest>(request, data: data, requestOptions: WebBrowser.ERequestOptions.ReturnRedirections | WebBrowser.ERequestOptions.ReturnClientErrors | WebBrowser.ERequestOptions.AllowInvalidBodyOnErrors | WebBrowser.ERequestOptions.CompressRequest).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
internal static async Task<ObjectResponse<GenericResponse<BackgroundTaskResponse>>?> AnnounceForListing(WebBrowser webBrowser, ulong steamID, IReadOnlyCollection<AssetForListing> inventory, string inventoryChecksum, IReadOnlyCollection<Asset.EType> acceptedMatchableTypes, uint totalInventoryCount, bool matchEverything, string tradeToken, string? nickname = null, string? avatarHash = null) {
|
||||
internal static async Task<ObjectResponse<GenericResponse<BackgroundTaskResponse>>?> AnnounceForListing(WebBrowser webBrowser, ulong steamID, IReadOnlyCollection<AssetForListing> inventory, string inventoryChecksum, IReadOnlyCollection<EAssetType> acceptedMatchableTypes, uint totalInventoryCount, bool matchEverything, string tradeToken, string? nickname = null, string? avatarHash = null) {
|
||||
ArgumentNullException.ThrowIfNull(webBrowser);
|
||||
|
||||
if ((steamID == 0) || !new SteamID(steamID).IsIndividualAccount) {
|
||||
@@ -114,7 +116,22 @@ internal static class Backend {
|
||||
return Utilities.GenerateChecksumFor(bytes);
|
||||
}
|
||||
|
||||
internal static async Task<(HttpStatusCode StatusCode, ImmutableHashSet<ListedUser> Users)?> GetListedUsersForMatching(Guid licenseID, Bot bot, WebBrowser webBrowser, IReadOnlyCollection<Asset> inventory, IReadOnlyCollection<Asset.EType> acceptedMatchableTypes) {
|
||||
internal static async Task<HttpStatusCode?> GetLicenseStatus(Guid licenseID, WebBrowser webBrowser) {
|
||||
ArgumentOutOfRangeException.ThrowIfEqual(licenseID, Guid.Empty);
|
||||
ArgumentNullException.ThrowIfNull(webBrowser);
|
||||
|
||||
Uri request = new(ArchiNet.URL, "/Api/Licenses/Status");
|
||||
|
||||
Dictionary<string, string> headers = new(1, StringComparer.Ordinal) {
|
||||
{ "X-License-Key", licenseID.ToString("N") }
|
||||
};
|
||||
|
||||
ObjectResponse<GenericResponse>? response = await webBrowser.UrlGetToJsonObject<GenericResponse>(request, headers, requestOptions: WebBrowser.ERequestOptions.ReturnClientErrors | WebBrowser.ERequestOptions.AllowInvalidBodyOnErrors).ConfigureAwait(false);
|
||||
|
||||
return response?.StatusCode;
|
||||
}
|
||||
|
||||
internal static async Task<(HttpStatusCode StatusCode, ImmutableHashSet<ListedUser> Users)?> GetListedUsersForMatching(Guid licenseID, Bot bot, WebBrowser webBrowser, IReadOnlyCollection<Asset> inventory, IReadOnlyCollection<EAssetType> acceptedMatchableTypes) {
|
||||
ArgumentOutOfRangeException.ThrowIfEqual(licenseID, Guid.Empty);
|
||||
ArgumentNullException.ThrowIfNull(bot);
|
||||
ArgumentNullException.ThrowIfNull(webBrowser);
|
||||
@@ -141,10 +158,10 @@ internal static class Backend {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (response.StatusCode, response.Content?.Result ?? ImmutableHashSet<ListedUser>.Empty);
|
||||
return (response.StatusCode, response.Content?.Result ?? []);
|
||||
}
|
||||
|
||||
internal static async Task<ObjectResponse<GenericResponse<ImmutableHashSet<SetPart>>>?> GetSetParts(WebBrowser webBrowser, ulong steamID, IReadOnlyCollection<Asset.EType> matchableTypes, IReadOnlyCollection<uint> realAppIDs, CancellationToken cancellationToken = default) {
|
||||
internal static async Task<ObjectResponse<GenericResponse<ImmutableHashSet<SetPart>>>?> GetSetParts(WebBrowser webBrowser, ulong steamID, IReadOnlyCollection<EAssetType> matchableTypes, IReadOnlyCollection<uint> realAppIDs, CancellationToken cancellationToken = default) {
|
||||
ArgumentNullException.ThrowIfNull(webBrowser);
|
||||
|
||||
if ((steamID == 0) || !new SteamID(steamID).IsIndividualAccount) {
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
// ----------------------------------------------------------------------------------------------
|
||||
// _ _ _ ____ _ _____
|
||||
// / \ _ __ ___ | |__ (_)/ ___| | |_ ___ __ _ _ __ ___ | ___|__ _ _ __ _ __ ___
|
||||
// / _ \ | '__|/ __|| '_ \ | |\___ \ | __|/ _ \ / _` || '_ ` _ \ | |_ / _` || '__|| '_ ` _ \
|
||||
// / ___ \ | | | (__ | | | || | ___) || |_| __/| (_| || | | | | || _|| (_| || | | | | | | |
|
||||
// /_/ \_\|_| \___||_| |_||_||____/ \__|\___| \__,_||_| |_| |_||_| \__,_||_| |_| |_| |_|
|
||||
// ----------------------------------------------------------------------------------------------
|
||||
// |
|
||||
// Copyright 2015-2023 Łukasz "JustArchi" Domeradzki
|
||||
// Copyright 2015-2024 Łukasz "JustArchi" Domeradzki
|
||||
// Contact: JustArchi@JustArchi.net
|
||||
// |
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
@@ -22,20 +24,22 @@
|
||||
using System;
|
||||
using System.Globalization;
|
||||
using System.IO;
|
||||
using System.Text.Json.Serialization;
|
||||
using System.Threading.Tasks;
|
||||
using ArchiSteamFarm.Collections;
|
||||
using ArchiSteamFarm.Core;
|
||||
using ArchiSteamFarm.Helpers;
|
||||
using ArchiSteamFarm.Helpers.Json;
|
||||
using ArchiSteamFarm.Localization;
|
||||
using ArchiSteamFarm.OfficialPlugins.ItemsMatcher.Data;
|
||||
using JetBrains.Annotations;
|
||||
using Newtonsoft.Json;
|
||||
|
||||
namespace ArchiSteamFarm.OfficialPlugins.ItemsMatcher;
|
||||
|
||||
internal sealed class BotCache : SerializableFile {
|
||||
[JsonProperty(Required = Required.DisallowNull)]
|
||||
internal readonly ConcurrentList<AssetForListing> LastAnnouncedAssetsForListing = new();
|
||||
[JsonDisallowNull]
|
||||
[JsonInclude]
|
||||
internal ConcurrentList<AssetForListing> LastAnnouncedAssetsForListing { get; private init; } = [];
|
||||
|
||||
internal string? LastAnnouncedTradeToken {
|
||||
get => BackingLastAnnouncedTradeToken;
|
||||
@@ -63,11 +67,27 @@ internal sealed class BotCache : SerializableFile {
|
||||
}
|
||||
}
|
||||
|
||||
[JsonProperty]
|
||||
private string? BackingLastAnnouncedTradeToken;
|
||||
internal DateTime? LastRequestAt {
|
||||
get => BackingLastRequestAt;
|
||||
|
||||
[JsonProperty]
|
||||
private string? BackingLastInventoryChecksumBeforeDeduplication;
|
||||
set {
|
||||
if (BackingLastRequestAt == value) {
|
||||
return;
|
||||
}
|
||||
|
||||
BackingLastRequestAt = value;
|
||||
Utilities.InBackground(Save);
|
||||
}
|
||||
}
|
||||
|
||||
[JsonInclude]
|
||||
private string? BackingLastAnnouncedTradeToken { get; set; }
|
||||
|
||||
[JsonInclude]
|
||||
private string? BackingLastInventoryChecksumBeforeDeduplication { get; set; }
|
||||
|
||||
[JsonInclude]
|
||||
private DateTime? BackingLastRequestAt { get; set; }
|
||||
|
||||
private BotCache(string filePath) : this() {
|
||||
ArgumentException.ThrowIfNullOrEmpty(filePath);
|
||||
@@ -84,6 +104,9 @@ internal sealed class BotCache : SerializableFile {
|
||||
[UsedImplicitly]
|
||||
public bool ShouldSerializeBackingLastInventoryChecksumBeforeDeduplication() => !string.IsNullOrEmpty(BackingLastInventoryChecksumBeforeDeduplication);
|
||||
|
||||
[UsedImplicitly]
|
||||
public bool ShouldSerializeBackingLastRequestAt() => BackingLastRequestAt.HasValue;
|
||||
|
||||
[UsedImplicitly]
|
||||
public bool ShouldSerializeLastAnnouncedAssetsForListing() => LastAnnouncedAssetsForListing.Count > 0;
|
||||
|
||||
@@ -97,6 +120,8 @@ internal sealed class BotCache : SerializableFile {
|
||||
base.Dispose(disposing);
|
||||
}
|
||||
|
||||
protected override Task Save() => Save(this);
|
||||
|
||||
internal static async Task<BotCache> CreateOrLoad(string filePath) {
|
||||
ArgumentException.ThrowIfNullOrEmpty(filePath);
|
||||
|
||||
@@ -115,7 +140,7 @@ internal sealed class BotCache : SerializableFile {
|
||||
return new BotCache(filePath);
|
||||
}
|
||||
|
||||
botCache = JsonConvert.DeserializeObject<BotCache>(json);
|
||||
botCache = json.ToJsonObject<BotCache>();
|
||||
} catch (Exception e) {
|
||||
ASF.ArchiLogger.LogGenericException(e);
|
||||
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
// ----------------------------------------------------------------------------------------------
|
||||
// _ _ _ ____ _ _____
|
||||
// / \ _ __ ___ | |__ (_)/ ___| | |_ ___ __ _ _ __ ___ | ___|__ _ _ __ _ __ ___
|
||||
// / _ \ | '__|/ __|| '_ \ | |\___ \ | __|/ _ \ / _` || '_ ` _ \ | |_ / _` || '__|| '_ ` _ \
|
||||
// / ___ \ | | | (__ | | | || | ___) || |_| __/| (_| || | | | | || _|| (_| || | | | | | | |
|
||||
// /_/ \_\|_| \___||_| |_||_||____/ \__|\___| \__,_||_| |_| |_||_| \__,_||_| |_| |_| |_|
|
||||
// ----------------------------------------------------------------------------------------------
|
||||
// |
|
||||
// Copyright 2015-2023 Łukasz "JustArchi" Domeradzki
|
||||
// Copyright 2015-2024 Łukasz "JustArchi" Domeradzki
|
||||
// Contact: JustArchi@JustArchi.net
|
||||
// |
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
@@ -80,12 +82,20 @@ internal static class Commands {
|
||||
return access > EAccess.None ? bot.Commands.FormatBotResponse(Strings.ErrorAccessDenied) : null;
|
||||
}
|
||||
|
||||
if (!bot.IsConnectedAndLoggedOn) {
|
||||
return bot.Commands.FormatBotResponse(Strings.BotNotConnected);
|
||||
}
|
||||
|
||||
if (bot.BotConfig.TradingPreferences.HasFlag(BotConfig.ETradingPreferences.MatchEverything)) {
|
||||
return bot.Commands.FormatBotResponse(string.Format(CultureInfo.CurrentCulture, Strings.WarningFailedWithError, nameof(BotConfig.ETradingPreferences.MatchEverything)));
|
||||
}
|
||||
|
||||
if ((ASF.GlobalConfig?.LicenseID == null) || (ASF.GlobalConfig.LicenseID == Guid.Empty)) {
|
||||
return bot.Commands.FormatBotResponse(string.Format(CultureInfo.CurrentCulture, Strings.WarningFailedWithError, nameof(ASF.GlobalConfig.LicenseID)));
|
||||
}
|
||||
|
||||
if (!bot.BotConfig.TradingPreferences.HasFlag(BotConfig.ETradingPreferences.MatchActively) || !ItemsMatcherPlugin.RemoteCommunications.TryGetValue(bot, out RemoteCommunication? remoteCommunication)) {
|
||||
return bot.Commands.FormatBotResponse(string.Format(CultureInfo.CurrentCulture, Strings.WarningFailedWithError, nameof(BotConfig.ETradingPreferences.MatchActively)));
|
||||
if (!ItemsMatcherPlugin.RemoteCommunications.TryGetValue(bot, out RemoteCommunication? remoteCommunication)) {
|
||||
return bot.Commands.FormatBotResponse(string.Format(CultureInfo.CurrentCulture, Strings.WarningFailedWithError, nameof(remoteCommunication)));
|
||||
}
|
||||
|
||||
remoteCommunication.TriggerMatchActivelyEarlier();
|
||||
@@ -112,7 +122,7 @@ internal static class Commands {
|
||||
|
||||
IList<string?> results = await Utilities.InParallel(bots.Select(bot => Task.Run(() => ResponseMatch(Steam.Interaction.Commands.GetProxyAccess(bot, access, steamID), bot)))).ConfigureAwait(false);
|
||||
|
||||
List<string> responses = new(results.Where(static result => !string.IsNullOrEmpty(result))!);
|
||||
List<string> responses = [..results.Where(static result => !string.IsNullOrEmpty(result))];
|
||||
|
||||
return responses.Count > 0 ? string.Join(Environment.NewLine, responses) : null;
|
||||
}
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
// ----------------------------------------------------------------------------------------------
|
||||
// _ _ _ ____ _ _____
|
||||
// / \ _ __ ___ | |__ (_)/ ___| | |_ ___ __ _ _ __ ___ | ___|__ _ _ __ _ __ ___
|
||||
// / _ \ | '__|/ __|| '_ \ | |\___ \ | __|/ _ \ / _` || '_ ` _ \ | |_ / _` || '__|| '_ ` _ \
|
||||
// / ___ \ | | | (__ | | | || | ___) || |_| __/| (_| || | | | | || _|| (_| || | | | | | | |
|
||||
// /_/ \_\|_| \___||_| |_||_||____/ \__|\___| \__,_||_| |_| |_||_| \__,_||_| |_| |_| |_|
|
||||
// ----------------------------------------------------------------------------------------------
|
||||
// |
|
||||
// Copyright 2015-2023 Łukasz "JustArchi" Domeradzki
|
||||
// Copyright 2015-2024 Łukasz "JustArchi" Domeradzki
|
||||
// Contact: JustArchi@JustArchi.net
|
||||
// |
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
@@ -22,21 +24,23 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.Immutable;
|
||||
using System.Text.Json.Serialization;
|
||||
using ArchiSteamFarm.Steam.Data;
|
||||
using ArchiSteamFarm.Steam.Storage;
|
||||
using Newtonsoft.Json;
|
||||
using SteamKit2;
|
||||
|
||||
namespace ArchiSteamFarm.OfficialPlugins.ItemsMatcher.Data;
|
||||
|
||||
internal sealed class AnnouncementDiffRequest : AnnouncementRequest {
|
||||
[JsonProperty(Required = Required.Always)]
|
||||
private readonly ImmutableHashSet<AssetForListing> InventoryRemoved;
|
||||
[JsonInclude]
|
||||
[JsonRequired]
|
||||
private ImmutableHashSet<AssetForListing> InventoryRemoved { get; init; }
|
||||
|
||||
[JsonProperty(Required = Required.Always)]
|
||||
private readonly string PreviousInventoryChecksum;
|
||||
[JsonInclude]
|
||||
[JsonRequired]
|
||||
private string PreviousInventoryChecksum { get; init; }
|
||||
|
||||
internal AnnouncementDiffRequest(Guid guid, ulong steamID, IReadOnlyCollection<AssetForListing> inventory, string inventoryChecksum, IReadOnlyCollection<Asset.EType> matchableTypes, uint totalInventoryCount, bool matchEverything, byte maxTradeHoldDuration, string tradeToken, IReadOnlyCollection<AssetForListing> inventoryRemoved, string previousInventoryChecksum, string? nickname = null, string? avatarHash = null) : base(guid, steamID, inventory, inventoryChecksum, matchableTypes, totalInventoryCount, matchEverything, maxTradeHoldDuration, tradeToken, nickname, avatarHash) {
|
||||
internal AnnouncementDiffRequest(Guid guid, ulong steamID, IReadOnlyCollection<AssetForListing> inventory, string inventoryChecksum, IReadOnlyCollection<EAssetType> matchableTypes, uint totalInventoryCount, bool matchEverything, byte maxTradeHoldDuration, string tradeToken, IReadOnlyCollection<AssetForListing> inventoryRemoved, string previousInventoryChecksum, string? nickname = null, string? avatarHash = null) : base(guid, steamID, inventory, inventoryChecksum, matchableTypes, totalInventoryCount, matchEverything, maxTradeHoldDuration, tradeToken, nickname, avatarHash) {
|
||||
ArgumentOutOfRangeException.ThrowIfEqual(guid, Guid.Empty);
|
||||
|
||||
if ((steamID == 0) || !new SteamID(steamID).IsIndividualAccount) {
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
// ----------------------------------------------------------------------------------------------
|
||||
// _ _ _ ____ _ _____
|
||||
// / \ _ __ ___ | |__ (_)/ ___| | |_ ___ __ _ _ __ ___ | ___|__ _ _ __ _ __ ___
|
||||
// / _ \ | '__|/ __|| '_ \ | |\___ \ | __|/ _ \ / _` || '_ ` _ \ | |_ / _` || '__|| '_ ` _ \
|
||||
// / ___ \ | | | (__ | | | || | ___) || |_| __/| (_| || | | | | || _|| (_| || | | | | | | |
|
||||
// /_/ \_\|_| \___||_| |_||_||____/ \__|\___| \__,_||_| |_| |_||_| \__,_||_| |_| |_| |_|
|
||||
// ----------------------------------------------------------------------------------------------
|
||||
// |
|
||||
// Copyright 2015-2023 Łukasz "JustArchi" Domeradzki
|
||||
// Copyright 2015-2024 Łukasz "JustArchi" Domeradzki
|
||||
// Contact: JustArchi@JustArchi.net
|
||||
// |
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
@@ -22,49 +24,58 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.Immutable;
|
||||
using System.Text.Json.Serialization;
|
||||
using ArchiSteamFarm.Steam.Data;
|
||||
using ArchiSteamFarm.Steam.Storage;
|
||||
using JetBrains.Annotations;
|
||||
using Newtonsoft.Json;
|
||||
using SteamKit2;
|
||||
|
||||
namespace ArchiSteamFarm.OfficialPlugins.ItemsMatcher.Data;
|
||||
|
||||
internal class AnnouncementRequest {
|
||||
[JsonProperty]
|
||||
private readonly string? AvatarHash;
|
||||
[JsonInclude]
|
||||
private string? AvatarHash { get; init; }
|
||||
|
||||
[JsonProperty(Required = Required.Always)]
|
||||
private readonly Guid Guid;
|
||||
[JsonInclude]
|
||||
[JsonRequired]
|
||||
private Guid Guid { get; init; }
|
||||
|
||||
[JsonProperty(Required = Required.Always)]
|
||||
private readonly ImmutableHashSet<AssetForListing> Inventory;
|
||||
[JsonInclude]
|
||||
[JsonRequired]
|
||||
private ImmutableHashSet<AssetForListing> Inventory { get; init; }
|
||||
|
||||
[JsonProperty(Required = Required.Always)]
|
||||
private readonly string InventoryChecksum;
|
||||
[JsonInclude]
|
||||
[JsonRequired]
|
||||
private string InventoryChecksum { get; init; }
|
||||
|
||||
[JsonProperty(Required = Required.Always)]
|
||||
private readonly ImmutableHashSet<Asset.EType> MatchableTypes;
|
||||
[JsonInclude]
|
||||
[JsonRequired]
|
||||
private ImmutableHashSet<EAssetType> MatchableTypes { get; init; }
|
||||
|
||||
[JsonProperty(Required = Required.Always)]
|
||||
private readonly bool MatchEverything;
|
||||
[JsonInclude]
|
||||
[JsonRequired]
|
||||
private bool MatchEverything { get; init; }
|
||||
|
||||
[JsonProperty(Required = Required.Always)]
|
||||
private readonly byte MaxTradeHoldDuration;
|
||||
[JsonInclude]
|
||||
[JsonRequired]
|
||||
private byte MaxTradeHoldDuration { get; init; }
|
||||
|
||||
[JsonProperty]
|
||||
private readonly string? Nickname;
|
||||
[JsonInclude]
|
||||
private string? Nickname { get; init; }
|
||||
|
||||
[JsonProperty(Required = Required.Always)]
|
||||
private readonly ulong SteamID;
|
||||
[JsonInclude]
|
||||
[JsonRequired]
|
||||
private ulong SteamID { get; init; }
|
||||
|
||||
[JsonProperty(Required = Required.Always)]
|
||||
private readonly uint TotalInventoryCount;
|
||||
[JsonInclude]
|
||||
[JsonRequired]
|
||||
private uint TotalInventoryCount { get; init; }
|
||||
|
||||
[JsonProperty(Required = Required.Always)]
|
||||
private readonly string TradeToken;
|
||||
[JsonInclude]
|
||||
[JsonRequired]
|
||||
private string TradeToken { get; init; }
|
||||
|
||||
internal AnnouncementRequest(Guid guid, ulong steamID, IReadOnlyCollection<AssetForListing> inventory, string inventoryChecksum, IReadOnlyCollection<Asset.EType> matchableTypes, uint totalInventoryCount, bool matchEverything, byte maxTradeHoldDuration, string tradeToken, string? nickname = null, string? avatarHash = null) {
|
||||
internal AnnouncementRequest(Guid guid, ulong steamID, IReadOnlyCollection<AssetForListing> inventory, string inventoryChecksum, IReadOnlyCollection<EAssetType> matchableTypes, uint totalInventoryCount, bool matchEverything, byte maxTradeHoldDuration, string tradeToken, string? nickname = null, string? avatarHash = null) {
|
||||
ArgumentOutOfRangeException.ThrowIfEqual(guid, Guid.Empty);
|
||||
|
||||
if ((steamID == 0) || !new SteamID(steamID).IsIndividualAccount) {
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
// ----------------------------------------------------------------------------------------------
|
||||
// _ _ _ ____ _ _____
|
||||
// / \ _ __ ___ | |__ (_)/ ___| | |_ ___ __ _ _ __ ___ | ___|__ _ _ __ _ __ ___
|
||||
// / _ \ | '__|/ __|| '_ \ | |\___ \ | __|/ _ \ / _` || '_ ` _ \ | |_ / _` || '__|| '_ ` _ \
|
||||
// / ___ \ | | | (__ | | | || | ___) || |_| __/| (_| || | | | | || _|| (_| || | | | | | | |
|
||||
// /_/ \_\|_| \___||_| |_||_||____/ \__|\___| \__,_||_| |_| |_||_| \__,_||_| |_| |_| |_|
|
||||
// ----------------------------------------------------------------------------------------------
|
||||
// |
|
||||
// Copyright 2015-2023 Łukasz "JustArchi" Domeradzki
|
||||
// Copyright 2015-2024 Łukasz "JustArchi" Domeradzki
|
||||
// Contact: JustArchi@JustArchi.net
|
||||
// |
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
@@ -20,19 +22,23 @@
|
||||
// limitations under the License.
|
||||
|
||||
using System;
|
||||
using System.Text.Json.Serialization;
|
||||
using ArchiSteamFarm.Steam.Data;
|
||||
using Newtonsoft.Json;
|
||||
|
||||
namespace ArchiSteamFarm.OfficialPlugins.ItemsMatcher.Data;
|
||||
|
||||
internal sealed class AssetForListing : AssetInInventory {
|
||||
[JsonProperty("i", Required = Required.Always)]
|
||||
internal readonly uint Index;
|
||||
internal string BackendHashCode => $"{Index}-{PreviousAssetID}-{AssetID}-{ClassID}-{Rarity}-{RealAppID}-{Tradable}-{Type}-{Amount}";
|
||||
|
||||
[JsonProperty("l", Required = Required.Always)]
|
||||
internal readonly ulong PreviousAssetID;
|
||||
[JsonInclude]
|
||||
[JsonPropertyName("i")]
|
||||
[JsonRequired]
|
||||
internal uint Index { get; private init; }
|
||||
|
||||
internal string BackendHashCode => Index + "-" + PreviousAssetID + "-" + AssetID + "-" + ClassID + "-" + Rarity + "-" + RealAppID + "-" + Tradable + "-" + Type + "-" + Amount;
|
||||
[JsonInclude]
|
||||
[JsonPropertyName("l")]
|
||||
[JsonRequired]
|
||||
internal ulong PreviousAssetID { get; private init; }
|
||||
|
||||
internal AssetForListing(Asset asset, uint index, ulong previousAssetID) : base(asset) {
|
||||
ArgumentNullException.ThrowIfNull(asset);
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
// ----------------------------------------------------------------------------------------------
|
||||
// _ _ _ ____ _ _____
|
||||
// / \ _ __ ___ | |__ (_)/ ___| | |_ ___ __ _ _ __ ___ | ___|__ _ _ __ _ __ ___
|
||||
// / _ \ | '__|/ __|| '_ \ | |\___ \ | __|/ _ \ / _` || '_ ` _ \ | |_ / _` || '__|| '_ ` _ \
|
||||
// / ___ \ | | | (__ | | | || | ___) || |_| __/| (_| || | | | | || _|| (_| || | | | | | | |
|
||||
// /_/ \_\|_| \___||_| |_||_||____/ \__|\___| \__,_||_| |_| |_||_| \__,_||_| |_| |_| |_|
|
||||
// ----------------------------------------------------------------------------------------------
|
||||
// |
|
||||
// Copyright 2015-2023 Łukasz "JustArchi" Domeradzki
|
||||
// Copyright 2015-2024 Łukasz "JustArchi" Domeradzki
|
||||
// Contact: JustArchi@JustArchi.net
|
||||
// |
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
@@ -20,30 +22,42 @@
|
||||
// limitations under the License.
|
||||
|
||||
using System;
|
||||
using System.Text.Json.Serialization;
|
||||
using ArchiSteamFarm.Steam.Data;
|
||||
using Newtonsoft.Json;
|
||||
|
||||
namespace ArchiSteamFarm.OfficialPlugins.ItemsMatcher.Data;
|
||||
|
||||
internal class AssetForMatching {
|
||||
[JsonProperty("c", Required = Required.Always)]
|
||||
internal readonly ulong ClassID;
|
||||
|
||||
[JsonProperty("r", Required = Required.Always)]
|
||||
internal readonly Asset.ERarity Rarity;
|
||||
|
||||
[JsonProperty("e", Required = Required.Always)]
|
||||
internal readonly uint RealAppID;
|
||||
|
||||
[JsonProperty("t", Required = Required.Always)]
|
||||
internal readonly bool Tradable;
|
||||
|
||||
[JsonProperty("p", Required = Required.Always)]
|
||||
internal readonly Asset.EType Type;
|
||||
|
||||
[JsonProperty("a", Required = Required.Always)]
|
||||
[JsonInclude]
|
||||
[JsonPropertyName("a")]
|
||||
[JsonRequired]
|
||||
internal uint Amount { get; set; }
|
||||
|
||||
[JsonInclude]
|
||||
[JsonPropertyName("c")]
|
||||
[JsonRequired]
|
||||
internal ulong ClassID { get; private init; }
|
||||
|
||||
[JsonInclude]
|
||||
[JsonPropertyName("r")]
|
||||
[JsonRequired]
|
||||
internal EAssetRarity Rarity { get; private init; }
|
||||
|
||||
[JsonInclude]
|
||||
[JsonPropertyName("e")]
|
||||
[JsonRequired]
|
||||
internal uint RealAppID { get; private init; }
|
||||
|
||||
[JsonInclude]
|
||||
[JsonPropertyName("t")]
|
||||
[JsonRequired]
|
||||
internal bool Tradable { get; private init; }
|
||||
|
||||
[JsonInclude]
|
||||
[JsonPropertyName("p")]
|
||||
[JsonRequired]
|
||||
internal EAssetType Type { get; private init; }
|
||||
|
||||
[JsonConstructor]
|
||||
protected AssetForMatching() { }
|
||||
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
// ----------------------------------------------------------------------------------------------
|
||||
// _ _ _ ____ _ _____
|
||||
// / \ _ __ ___ | |__ (_)/ ___| | |_ ___ __ _ _ __ ___ | ___|__ _ _ __ _ __ ___
|
||||
// / _ \ | '__|/ __|| '_ \ | |\___ \ | __|/ _ \ / _` || '_ ` _ \ | |_ / _` || '__|| '_ ` _ \
|
||||
// / ___ \ | | | (__ | | | || | ___) || |_| __/| (_| || | | | | || _|| (_| || | | | | | | |
|
||||
// /_/ \_\|_| \___||_| |_||_||____/ \__|\___| \__,_||_| |_| |_||_| \__,_||_| |_| |_| |_|
|
||||
// ----------------------------------------------------------------------------------------------
|
||||
// |
|
||||
// Copyright 2015-2023 Łukasz "JustArchi" Domeradzki
|
||||
// Copyright 2015-2024 Łukasz "JustArchi" Domeradzki
|
||||
// Contact: JustArchi@JustArchi.net
|
||||
// |
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
@@ -20,14 +22,16 @@
|
||||
// limitations under the License.
|
||||
|
||||
using System;
|
||||
using System.Text.Json.Serialization;
|
||||
using ArchiSteamFarm.Steam.Data;
|
||||
using Newtonsoft.Json;
|
||||
|
||||
namespace ArchiSteamFarm.OfficialPlugins.ItemsMatcher.Data;
|
||||
|
||||
internal class AssetInInventory : AssetForMatching {
|
||||
[JsonProperty("d", Required = Required.Always)]
|
||||
internal readonly ulong AssetID;
|
||||
[JsonInclude]
|
||||
[JsonPropertyName("d")]
|
||||
[JsonRequired]
|
||||
internal ulong AssetID { get; private init; }
|
||||
|
||||
[JsonConstructor]
|
||||
protected AssetInInventory() { }
|
||||
@@ -38,5 +42,5 @@ internal class AssetInInventory : AssetForMatching {
|
||||
AssetID = asset.AssetID;
|
||||
}
|
||||
|
||||
internal Asset ToAsset() => new(Asset.SteamAppID, Asset.SteamCommunityContextID, ClassID, Amount, tradable: Tradable, assetID: AssetID, realAppID: RealAppID, type: Type, rarity: Rarity);
|
||||
internal Asset ToAsset() => new(Asset.SteamAppID, Asset.SteamCommunityContextID, ClassID, Amount, new InventoryDescription(Asset.SteamAppID, ClassID, tradable: false, realAppID: RealAppID, type: Type, rarity: Rarity), AssetID);
|
||||
}
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
// ----------------------------------------------------------------------------------------------
|
||||
// _ _ _ ____ _ _____
|
||||
// / \ _ __ ___ | |__ (_)/ ___| | |_ ___ __ _ _ __ ___ | ___|__ _ _ __ _ __ ___
|
||||
// / _ \ | '__|/ __|| '_ \ | |\___ \ | __|/ _ \ / _` || '_ ` _ \ | |_ / _` || '__|| '_ ` _ \
|
||||
// / ___ \ | | | (__ | | | || | ___) || |_| __/| (_| || | | | | || _|| (_| || | | | | | | |
|
||||
// /_/ \_\|_| \___||_| |_||_||____/ \__|\___| \__,_||_| |_| |_||_| \__,_||_| |_| |_| |_|
|
||||
// ----------------------------------------------------------------------------------------------
|
||||
// |
|
||||
// Copyright 2015-2023 Łukasz "JustArchi" Domeradzki
|
||||
// Copyright 2015-2024 Łukasz "JustArchi" Domeradzki
|
||||
// Contact: JustArchi@JustArchi.net
|
||||
// |
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
@@ -21,22 +23,20 @@
|
||||
|
||||
using System;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using Newtonsoft.Json;
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace ArchiSteamFarm.OfficialPlugins.ItemsMatcher.Data;
|
||||
|
||||
#pragma warning disable CA1812 // False positive, the class is used during json deserialization
|
||||
[SuppressMessage("ReSharper", "ClassCannotBeInstantiated")]
|
||||
internal sealed class BackgroundTaskResponse {
|
||||
#pragma warning disable CS0649 // False positive, the field is used during json deserialization
|
||||
[JsonProperty(Required = Required.Always)]
|
||||
internal readonly bool Finished;
|
||||
#pragma warning restore CS0649 // False positive, the field is used during json deserialization
|
||||
[JsonInclude]
|
||||
[JsonRequired]
|
||||
internal bool Finished { get; private init; }
|
||||
|
||||
#pragma warning disable CS0649 // False positive, the field is used during json deserialization
|
||||
[JsonProperty(Required = Required.Always)]
|
||||
internal readonly Guid RequestID;
|
||||
#pragma warning restore CS0649 // False positive, the field is used during json deserialization
|
||||
[JsonInclude]
|
||||
[JsonRequired]
|
||||
internal Guid RequestID { get; private init; }
|
||||
|
||||
[JsonConstructor]
|
||||
private BackgroundTaskResponse() { }
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
// ----------------------------------------------------------------------------------------------
|
||||
// _ _ _ ____ _ _____
|
||||
// / \ _ __ ___ | |__ (_)/ ___| | |_ ___ __ _ _ __ ___ | ___|__ _ _ __ _ __ ___
|
||||
// / _ \ | '__|/ __|| '_ \ | |\___ \ | __|/ _ \ / _` || '_ ` _ \ | |_ / _` || '__|| '_ ` _ \
|
||||
// / ___ \ | | | (__ | | | || | ___) || |_| __/| (_| || | | | | || _|| (_| || | | | | | | |
|
||||
// /_/ \_\|_| \___||_| |_||_||____/ \__|\___| \__,_||_| |_| |_||_| \__,_||_| |_| |_| |_|
|
||||
// ----------------------------------------------------------------------------------------------
|
||||
// |
|
||||
// Copyright 2015-2023 Łukasz "JustArchi" Domeradzki
|
||||
// Copyright 2015-2024 Łukasz "JustArchi" Domeradzki
|
||||
// Contact: JustArchi@JustArchi.net
|
||||
// |
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
@@ -20,17 +22,19 @@
|
||||
// limitations under the License.
|
||||
|
||||
using System;
|
||||
using Newtonsoft.Json;
|
||||
using System.Text.Json.Serialization;
|
||||
using SteamKit2;
|
||||
|
||||
namespace ArchiSteamFarm.OfficialPlugins.ItemsMatcher.Data;
|
||||
|
||||
internal sealed class HeartBeatRequest {
|
||||
[JsonProperty(Required = Required.Always)]
|
||||
internal readonly Guid Guid;
|
||||
[JsonInclude]
|
||||
[JsonRequired]
|
||||
internal Guid Guid { get; private init; }
|
||||
|
||||
[JsonProperty(Required = Required.Always)]
|
||||
internal readonly ulong SteamID;
|
||||
[JsonInclude]
|
||||
[JsonRequired]
|
||||
internal ulong SteamID { get; private init; }
|
||||
|
||||
internal HeartBeatRequest(Guid guid, ulong steamID) {
|
||||
ArgumentOutOfRangeException.ThrowIfEqual(guid, Guid.Empty);
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
// ----------------------------------------------------------------------------------------------
|
||||
// _ _ _ ____ _ _____
|
||||
// / \ _ __ ___ | |__ (_)/ ___| | |_ ___ __ _ _ __ ___ | ___|__ _ _ __ _ __ ___
|
||||
// / _ \ | '__|/ __|| '_ \ | |\___ \ | __|/ _ \ / _` || '_ ` _ \ | |_ / _` || '__|| '_ ` _ \
|
||||
// / ___ \ | | | (__ | | | || | ___) || |_| __/| (_| || | | | | || _|| (_| || | | | | | | |
|
||||
// /_/ \_\|_| \___||_| |_||_||____/ \__|\___| \__,_||_| |_| |_||_| \__,_||_| |_| |_| |_|
|
||||
// ----------------------------------------------------------------------------------------------
|
||||
// |
|
||||
// Copyright 2015-2023 Łukasz "JustArchi" Domeradzki
|
||||
// Copyright 2015-2024 Łukasz "JustArchi" Domeradzki
|
||||
// Contact: JustArchi@JustArchi.net
|
||||
// |
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
@@ -23,26 +25,30 @@ using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.Immutable;
|
||||
using System.Linq;
|
||||
using System.Text.Json.Serialization;
|
||||
using ArchiSteamFarm.Steam.Data;
|
||||
using Newtonsoft.Json;
|
||||
using SteamKit2;
|
||||
|
||||
namespace ArchiSteamFarm.OfficialPlugins.ItemsMatcher.Data;
|
||||
|
||||
internal sealed class InventoriesRequest {
|
||||
[JsonProperty(Required = Required.Always)]
|
||||
internal readonly Guid Guid;
|
||||
[JsonInclude]
|
||||
[JsonRequired]
|
||||
internal Guid Guid { get; private init; }
|
||||
|
||||
[JsonProperty(Required = Required.Always)]
|
||||
internal readonly ImmutableHashSet<AssetForMatching> Inventory;
|
||||
[JsonInclude]
|
||||
[JsonRequired]
|
||||
internal ImmutableHashSet<AssetForMatching> Inventory { get; private init; }
|
||||
|
||||
[JsonProperty(Required = Required.Always)]
|
||||
internal readonly ImmutableHashSet<Asset.EType> MatchableTypes;
|
||||
[JsonInclude]
|
||||
[JsonRequired]
|
||||
internal ImmutableHashSet<EAssetType> MatchableTypes { get; private init; }
|
||||
|
||||
[JsonProperty(Required = Required.Always)]
|
||||
internal readonly ulong SteamID;
|
||||
[JsonInclude]
|
||||
[JsonRequired]
|
||||
internal ulong SteamID { get; private init; }
|
||||
|
||||
internal InventoriesRequest(Guid guid, ulong steamID, IReadOnlyCollection<Asset> inventory, IReadOnlyCollection<Asset.EType> matchableTypes) {
|
||||
internal InventoriesRequest(Guid guid, ulong steamID, IReadOnlyCollection<Asset> inventory, IReadOnlyCollection<EAssetType> matchableTypes) {
|
||||
ArgumentOutOfRangeException.ThrowIfEqual(guid, Guid.Empty);
|
||||
|
||||
if ((steamID == 0) || !new SteamID(steamID).IsIndividualAccount) {
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
// ----------------------------------------------------------------------------------------------
|
||||
// _ _ _ ____ _ _____
|
||||
// / \ _ __ ___ | |__ (_)/ ___| | |_ ___ __ _ _ __ ___ | ___|__ _ _ __ _ __ ___
|
||||
// / _ \ | '__|/ __|| '_ \ | |\___ \ | __|/ _ \ / _` || '_ ` _ \ | |_ / _` || '__|| '_ ` _ \
|
||||
// / ___ \ | | | (__ | | | || | ___) || |_| __/| (_| || | | | | || _|| (_| || | | | | | | |
|
||||
// /_/ \_\|_| \___||_| |_||_||____/ \__|\___| \__,_||_| |_| |_||_| \__,_||_| |_| |_| |_|
|
||||
// ----------------------------------------------------------------------------------------------
|
||||
// |
|
||||
// Copyright 2015-2023 Łukasz "JustArchi" Domeradzki
|
||||
// Copyright 2015-2024 Łukasz "JustArchi" Domeradzki
|
||||
// Contact: JustArchi@JustArchi.net
|
||||
// |
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
@@ -21,47 +23,48 @@
|
||||
|
||||
using System.Collections.Immutable;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.Text.Json.Serialization;
|
||||
using ArchiSteamFarm.Steam.Data;
|
||||
using Newtonsoft.Json;
|
||||
|
||||
namespace ArchiSteamFarm.OfficialPlugins.ItemsMatcher.Data;
|
||||
|
||||
#pragma warning disable CA1812 // False positive, the class is used during json deserialization
|
||||
[SuppressMessage("ReSharper", "ClassCannotBeInstantiated")]
|
||||
internal sealed class ListedUser {
|
||||
[JsonProperty(Required = Required.Always)]
|
||||
internal readonly ImmutableHashSet<AssetInInventory> Assets = ImmutableHashSet<AssetInInventory>.Empty;
|
||||
[JsonInclude]
|
||||
[JsonRequired]
|
||||
internal ImmutableHashSet<AssetInInventory> Assets { get; private init; } = [];
|
||||
|
||||
[JsonProperty(Required = Required.Always)]
|
||||
internal readonly ImmutableHashSet<Asset.EType> MatchableTypes = ImmutableHashSet<Asset.EType>.Empty;
|
||||
[JsonInclude]
|
||||
[JsonRequired]
|
||||
internal ImmutableHashSet<EAssetType> MatchableTypes { get; private init; } = [];
|
||||
|
||||
#pragma warning disable CS0649 // False positive, the field is used during json deserialization
|
||||
[JsonProperty(Required = Required.Always)]
|
||||
internal readonly bool MatchEverything;
|
||||
#pragma warning restore CS0649 // False positive, the field is used during json deserialization
|
||||
[JsonInclude]
|
||||
[JsonRequired]
|
||||
internal bool MatchEverything { get; private init; }
|
||||
|
||||
#pragma warning disable CS0649 // False positive, the field is used during json deserialization
|
||||
[JsonProperty(Required = Required.Always)]
|
||||
internal readonly byte MaxTradeHoldDuration;
|
||||
#pragma warning restore CS0649 // False positive, the field is used during json deserialization
|
||||
[JsonInclude]
|
||||
[JsonRequired]
|
||||
internal byte MaxTradeHoldDuration { get; private init; }
|
||||
|
||||
#pragma warning disable CS0649 // False positive, the field is used during json deserialization
|
||||
[JsonProperty(Required = Required.AllowNull)]
|
||||
internal readonly string? Nickname;
|
||||
#pragma warning restore CS0649 // False positive, the field is used during json deserialization
|
||||
[JsonInclude]
|
||||
internal string? Nickname { get; private init; }
|
||||
|
||||
#pragma warning disable CS0649 // False positive, the field is used during json deserialization
|
||||
[JsonProperty(Required = Required.Always)]
|
||||
internal readonly ulong SteamID;
|
||||
#pragma warning restore CS0649 // False positive, the field is used during json deserialization
|
||||
[JsonInclude]
|
||||
[JsonRequired]
|
||||
internal ulong SteamID { get; private init; }
|
||||
|
||||
#pragma warning disable CS0649 // False positive, the field is used during json deserialization
|
||||
[JsonProperty(Required = Required.Always)]
|
||||
internal readonly uint TotalInventoryCount;
|
||||
#pragma warning restore CS0649 // False positive, the field is used during json deserialization
|
||||
[JsonInclude]
|
||||
[JsonRequired]
|
||||
internal uint TotalGamesCount { get; private init; }
|
||||
|
||||
[JsonProperty(Required = Required.Always)]
|
||||
internal readonly string TradeToken = "";
|
||||
[JsonInclude]
|
||||
[JsonRequired]
|
||||
internal uint TotalInventoryCount { get; private init; }
|
||||
|
||||
[JsonInclude]
|
||||
[JsonRequired]
|
||||
internal string TradeToken { get; private init; } = "";
|
||||
|
||||
[JsonConstructor]
|
||||
private ListedUser() { }
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
// ----------------------------------------------------------------------------------------------
|
||||
// _ _ _ ____ _ _____
|
||||
// / \ _ __ ___ | |__ (_)/ ___| | |_ ___ __ _ _ __ ___ | ___|__ _ _ __ _ __ ___
|
||||
// / _ \ | '__|/ __|| '_ \ | |\___ \ | __|/ _ \ / _` || '_ ` _ \ | |_ / _` || '__|| '_ ` _ \
|
||||
// / ___ \ | | | (__ | | | || | ___) || |_| __/| (_| || | | | | || _|| (_| || | | | | | | |
|
||||
// /_/ \_\|_| \___||_| |_||_||____/ \__|\___| \__,_||_| |_| |_||_| \__,_||_| |_| |_| |_|
|
||||
// ----------------------------------------------------------------------------------------------
|
||||
// |
|
||||
// Copyright 2015-2023 Łukasz "JustArchi" Domeradzki
|
||||
// Copyright 2015-2024 Łukasz "JustArchi" Domeradzki
|
||||
// Contact: JustArchi@JustArchi.net
|
||||
// |
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
@@ -20,33 +22,33 @@
|
||||
// limitations under the License.
|
||||
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.Text.Json.Serialization;
|
||||
using ArchiSteamFarm.Steam.Data;
|
||||
using Newtonsoft.Json;
|
||||
|
||||
namespace ArchiSteamFarm.OfficialPlugins.ItemsMatcher.Data;
|
||||
|
||||
#pragma warning disable CA1812 // False positive, the class is used during json deserialization
|
||||
[SuppressMessage("ReSharper", "ClassCannotBeInstantiated")]
|
||||
internal sealed class SetPart {
|
||||
#pragma warning disable CS0649 // False positive, the field is used during json deserialization
|
||||
[JsonProperty("c", Required = Required.Always)]
|
||||
internal readonly ulong ClassID;
|
||||
#pragma warning restore CS0649 // False positive, the field is used during json deserialization
|
||||
[JsonInclude]
|
||||
[JsonPropertyName("c")]
|
||||
[JsonRequired]
|
||||
internal ulong ClassID { get; private init; }
|
||||
|
||||
#pragma warning disable CS0649 // False positive, the field is used during json deserialization
|
||||
[JsonProperty("r", Required = Required.Always)]
|
||||
internal readonly Asset.ERarity Rarity;
|
||||
#pragma warning restore CS0649 // False positive, the field is used during json deserialization
|
||||
[JsonInclude]
|
||||
[JsonPropertyName("r")]
|
||||
[JsonRequired]
|
||||
internal EAssetRarity Rarity { get; private init; }
|
||||
|
||||
#pragma warning disable CS0649 // False positive, the field is used during json deserialization
|
||||
[JsonProperty("e", Required = Required.Always)]
|
||||
internal readonly uint RealAppID;
|
||||
#pragma warning restore CS0649 // False positive, the field is used during json deserialization
|
||||
[JsonInclude]
|
||||
[JsonPropertyName("e")]
|
||||
[JsonRequired]
|
||||
internal uint RealAppID { get; private init; }
|
||||
|
||||
#pragma warning disable CS0649 // False positive, the field is used during json deserialization
|
||||
[JsonProperty("p", Required = Required.Always)]
|
||||
internal readonly Asset.EType Type;
|
||||
#pragma warning restore CS0649 // False positive, the field is used during json deserialization
|
||||
[JsonInclude]
|
||||
[JsonPropertyName("p")]
|
||||
[JsonRequired]
|
||||
internal EAssetType Type { get; private init; }
|
||||
|
||||
[JsonConstructor]
|
||||
private SetPart() { }
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
// ----------------------------------------------------------------------------------------------
|
||||
// _ _ _ ____ _ _____
|
||||
// / \ _ __ ___ | |__ (_)/ ___| | |_ ___ __ _ _ __ ___ | ___|__ _ _ __ _ __ ___
|
||||
// / _ \ | '__|/ __|| '_ \ | |\___ \ | __|/ _ \ / _` || '_ ` _ \ | |_ / _` || '__|| '_ ` _ \
|
||||
// / ___ \ | | | (__ | | | || | ___) || |_| __/| (_| || | | | | || _|| (_| || | | | | | | |
|
||||
// /_/ \_\|_| \___||_| |_||_||____/ \__|\___| \__,_||_| |_| |_||_| \__,_||_| |_| |_| |_|
|
||||
// ----------------------------------------------------------------------------------------------
|
||||
// |
|
||||
// Copyright 2015-2023 Łukasz "JustArchi" Domeradzki
|
||||
// Copyright 2015-2024 Łukasz "JustArchi" Domeradzki
|
||||
// Contact: JustArchi@JustArchi.net
|
||||
// |
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
@@ -22,26 +24,30 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.Immutable;
|
||||
using System.Text.Json.Serialization;
|
||||
using ArchiSteamFarm.Steam.Data;
|
||||
using Newtonsoft.Json;
|
||||
using SteamKit2;
|
||||
|
||||
namespace ArchiSteamFarm.OfficialPlugins.ItemsMatcher.Data;
|
||||
|
||||
internal sealed class SetPartsRequest {
|
||||
[JsonProperty(Required = Required.Always)]
|
||||
internal readonly Guid Guid;
|
||||
[JsonInclude]
|
||||
[JsonRequired]
|
||||
internal Guid Guid { get; private init; }
|
||||
|
||||
[JsonProperty(Required = Required.Always)]
|
||||
internal readonly ImmutableHashSet<Asset.EType> MatchableTypes;
|
||||
[JsonInclude]
|
||||
[JsonRequired]
|
||||
internal ImmutableHashSet<EAssetType> MatchableTypes { get; private init; }
|
||||
|
||||
[JsonProperty(Required = Required.Always)]
|
||||
internal readonly ImmutableHashSet<uint> RealAppIDs;
|
||||
[JsonInclude]
|
||||
[JsonRequired]
|
||||
internal ImmutableHashSet<uint> RealAppIDs { get; private init; }
|
||||
|
||||
[JsonProperty(Required = Required.Always)]
|
||||
internal readonly ulong SteamID;
|
||||
[JsonInclude]
|
||||
[JsonRequired]
|
||||
internal ulong SteamID { get; private init; }
|
||||
|
||||
internal SetPartsRequest(Guid guid, ulong steamID, IReadOnlyCollection<Asset.EType> matchableTypes, IReadOnlyCollection<uint> realAppIDs) {
|
||||
internal SetPartsRequest(Guid guid, ulong steamID, IReadOnlyCollection<EAssetType> matchableTypes, IReadOnlyCollection<uint> realAppIDs) {
|
||||
ArgumentOutOfRangeException.ThrowIfEqual(guid, Guid.Empty);
|
||||
|
||||
if ((steamID == 0) || !new SteamID(steamID).IsIndividualAccount) {
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
// ----------------------------------------------------------------------------------------------
|
||||
// _ _ _ ____ _ _____
|
||||
// / \ _ __ ___ | |__ (_)/ ___| | |_ ___ __ _ _ __ ___ | ___|__ _ _ __ _ __ ___
|
||||
// / _ \ | '__|/ __|| '_ \ | |\___ \ | __|/ _ \ / _` || '_ ` _ \ | |_ / _` || '__|| '_ ` _ \
|
||||
// / ___ \ | | | (__ | | | || | ___) || |_| __/| (_| || | | | | || _|| (_| || | | | | | | |
|
||||
// /_/ \_\|_| \___||_| |_||_||____/ \__|\___| \__,_||_| |_| |_||_| \__,_||_| |_| |_| |_|
|
||||
// ----------------------------------------------------------------------------------------------
|
||||
// |
|
||||
// Copyright 2015-2023 Łukasz "JustArchi" Domeradzki
|
||||
// Copyright 2015-2024 Łukasz "JustArchi" Domeradzki
|
||||
// Contact: JustArchi@JustArchi.net
|
||||
// |
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
@@ -23,8 +25,11 @@ using System;
|
||||
using System.Collections.Concurrent;
|
||||
using System.Collections.Generic;
|
||||
using System.ComponentModel;
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using System.Composition;
|
||||
using System.Linq;
|
||||
using System.Text.Json;
|
||||
using System.Text.Json.Serialization;
|
||||
using System.Threading.Tasks;
|
||||
using ArchiSteamFarm.Core;
|
||||
using ArchiSteamFarm.OfficialPlugins.ItemsMatcher.Localization;
|
||||
@@ -33,8 +38,6 @@ using ArchiSteamFarm.Plugins.Interfaces;
|
||||
using ArchiSteamFarm.Steam;
|
||||
using ArchiSteamFarm.Steam.Exchange;
|
||||
using ArchiSteamFarm.Steam.Integration.Callbacks;
|
||||
using Newtonsoft.Json;
|
||||
using Newtonsoft.Json.Linq;
|
||||
using SteamKit2;
|
||||
|
||||
namespace ArchiSteamFarm.OfficialPlugins.ItemsMatcher;
|
||||
@@ -43,10 +46,12 @@ namespace ArchiSteamFarm.OfficialPlugins.ItemsMatcher;
|
||||
internal sealed class ItemsMatcherPlugin : OfficialPlugin, IBot, IBotCommand2, IBotIdentity, IBotModules, IBotTradeOfferResults, IBotUserNotifications {
|
||||
internal static readonly ConcurrentDictionary<Bot, RemoteCommunication> RemoteCommunications = new();
|
||||
|
||||
[JsonProperty]
|
||||
[JsonInclude]
|
||||
[Required]
|
||||
public override string Name => nameof(ItemsMatcherPlugin);
|
||||
|
||||
[JsonProperty]
|
||||
[JsonInclude]
|
||||
[Required]
|
||||
public override Version Version => typeof(ItemsMatcherPlugin).Assembly.GetName().Version ?? throw new InvalidOperationException(nameof(Version));
|
||||
|
||||
public async Task<string?> OnBotCommand(Bot bot, EAccess access, string message, string[] args, ulong steamID = 0) {
|
||||
@@ -83,7 +88,7 @@ internal sealed class ItemsMatcherPlugin : OfficialPlugin, IBot, IBotCommand2, I
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
public async Task OnBotInitModules(Bot bot, IReadOnlyDictionary<string, JToken>? additionalConfigProperties = null) {
|
||||
public async Task OnBotInitModules(Bot bot, IReadOnlyDictionary<string, JsonElement>? additionalConfigProperties = null) {
|
||||
ArgumentNullException.ThrowIfNull(bot);
|
||||
|
||||
if (RemoteCommunications.TryRemove(bot, out RemoteCommunication? remoteCommunication)) {
|
||||
|
||||
@@ -82,7 +82,4 @@
|
||||
<value>Einige Bestätigungen sind fehlgeschlagen. Lediglich {0} von {1} Transaktionen wurden erfolgreich versendet.</value>
|
||||
<comment>{0} will be replaced by amount of the trade offers that succeeded (number), {1} will be replaced by amount of the trade offers that were supposed to be sent in total (number)</comment>
|
||||
</data>
|
||||
<data name="PluginDisabledCustomBuild" xml:space="preserve">
|
||||
<value>ItemsMatcherPlugin wird in benutzerdefinierten ASF-Versionen nicht unterstützt, die Funktionalität ist deaktiviert.</value>
|
||||
</data>
|
||||
</root>
|
||||
|
||||
@@ -82,7 +82,4 @@
|
||||
<value>Algunas confirmaciones han fallado, aproximadamente {0} de {1} intercambios fueron enviados exitosamente.</value>
|
||||
<comment>{0} will be replaced by amount of the trade offers that succeeded (number), {1} will be replaced by amount of the trade offers that were supposed to be sent in total (number)</comment>
|
||||
</data>
|
||||
<data name="PluginDisabledCustomBuild" xml:space="preserve">
|
||||
<value>ItemsMatcherPlugin no está soportado en compilaciones personalizadas de ASF, la función está deshabilitada</value>
|
||||
</data>
|
||||
</root>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -82,7 +82,4 @@
|
||||
<value>Niektóre potwierdzenia nie powiodły się, około {0} z {1} transakcji zostało wysłanych pomyślnie.</value>
|
||||
<comment>{0} will be replaced by amount of the trade offers that succeeded (number), {1} will be replaced by amount of the trade offers that were supposed to be sent in total (number)</comment>
|
||||
</data>
|
||||
<data name="PluginDisabledCustomBuild" xml:space="preserve">
|
||||
<value>Wtyczka ItemsMatcherPlugin nie jest obsługiwana w niestandardowych kompilacjach ASF, funkcjonalność jest wyłączona.</value>
|
||||
</data>
|
||||
</root>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -82,7 +82,4 @@
|
||||
<value>部分确认失败,{1} 个交易中约有 {0} 个发送成功。</value>
|
||||
<comment>{0} will be replaced by amount of the trade offers that succeeded (number), {1} will be replaced by amount of the trade offers that were supposed to be sent in total (number)</comment>
|
||||
</data>
|
||||
<data name="PluginDisabledCustomBuild" xml:space="preserve">
|
||||
<value>ItemsMatcherPlugin 不支持自定义的 ASF 构建,此功能已禁用。</value>
|
||||
</data>
|
||||
</root>
|
||||
|
||||
@@ -82,7 +82,4 @@
|
||||
<value>部分交易失敗,{1} 個交易中約 {0} 個發送成功。</value>
|
||||
<comment>{0} will be replaced by amount of the trade offers that succeeded (number), {1} will be replaced by amount of the trade offers that were supposed to be sent in total (number)</comment>
|
||||
</data>
|
||||
<data name="PluginDisabledCustomBuild" xml:space="preserve">
|
||||
<value>ItemsMatcherPlugin 在自訂的 ASF 組建中不受支援,已停止功能。</value>
|
||||
</data>
|
||||
</root>
|
||||
|
||||
191
ArchiSteamFarm.OfficialPlugins.ItemsMatcher/MatchingUtilities.cs
Normal file
191
ArchiSteamFarm.OfficialPlugins.ItemsMatcher/MatchingUtilities.cs
Normal file
@@ -0,0 +1,191 @@
|
||||
// ----------------------------------------------------------------------------------------------
|
||||
// _ _ _ ____ _ _____
|
||||
// / \ _ __ ___ | |__ (_)/ ___| | |_ ___ __ _ _ __ ___ | ___|__ _ _ __ _ __ ___
|
||||
// / _ \ | '__|/ __|| '_ \ | |\___ \ | __|/ _ \ / _` || '_ ` _ \ | |_ / _` || '__|| '_ ` _ \
|
||||
// / ___ \ | | | (__ | | | || | ___) || |_| __/| (_| || | | | | || _|| (_| || | | | | | | |
|
||||
// /_/ \_\|_| \___||_| |_||_||____/ \__|\___| \__,_||_| |_| |_||_| \__,_||_| |_| |_| |_|
|
||||
// ----------------------------------------------------------------------------------------------
|
||||
// |
|
||||
// Copyright 2015-2024 Łukasz "JustArchi" Domeradzki
|
||||
// Contact: JustArchi@JustArchi.net
|
||||
// |
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
// |
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
// |
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using ArchiSteamFarm.Steam.Data;
|
||||
|
||||
namespace ArchiSteamFarm.OfficialPlugins.ItemsMatcher;
|
||||
|
||||
internal static class MatchingUtilities {
|
||||
internal static (Dictionary<(uint RealAppID, EAssetType Type, EAssetRarity Rarity), Dictionary<ulong, uint>> FullState, Dictionary<(uint RealAppID, EAssetType Type, EAssetRarity Rarity), Dictionary<ulong, uint>> TradableState) GetDividedInventoryState(IReadOnlyCollection<Asset> inventory) {
|
||||
if ((inventory == null) || (inventory.Count == 0)) {
|
||||
throw new ArgumentNullException(nameof(inventory));
|
||||
}
|
||||
|
||||
Dictionary<(uint RealAppID, EAssetType Type, EAssetRarity Rarity), Dictionary<ulong, uint>> fullState = new();
|
||||
Dictionary<(uint RealAppID, EAssetType Type, EAssetRarity Rarity), Dictionary<ulong, uint>> tradableState = new();
|
||||
|
||||
foreach (Asset item in inventory) {
|
||||
(uint RealAppID, EAssetType Type, EAssetRarity Rarity) key = (item.RealAppID, item.Type, item.Rarity);
|
||||
|
||||
if (fullState.TryGetValue(key, out Dictionary<ulong, uint>? fullSet)) {
|
||||
fullSet[item.ClassID] = fullSet.GetValueOrDefault(item.ClassID) + item.Amount;
|
||||
} else {
|
||||
fullState[key] = new Dictionary<ulong, uint> { { item.ClassID, item.Amount } };
|
||||
}
|
||||
|
||||
if (!item.Tradable) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (tradableState.TryGetValue(key, out Dictionary<ulong, uint>? tradableSet)) {
|
||||
tradableSet[item.ClassID] = tradableSet.GetValueOrDefault(item.ClassID) + item.Amount;
|
||||
} else {
|
||||
tradableState[key] = new Dictionary<ulong, uint> { { item.ClassID, item.Amount } };
|
||||
}
|
||||
}
|
||||
|
||||
return (fullState, tradableState);
|
||||
}
|
||||
|
||||
internal static Dictionary<(uint RealAppID, EAssetType Type, EAssetRarity Rarity), Dictionary<ulong, uint>> GetTradableInventoryState(IReadOnlyCollection<Asset> inventory) {
|
||||
if ((inventory == null) || (inventory.Count == 0)) {
|
||||
throw new ArgumentNullException(nameof(inventory));
|
||||
}
|
||||
|
||||
Dictionary<(uint RealAppID, EAssetType Type, EAssetRarity Rarity), Dictionary<ulong, uint>> tradableState = new();
|
||||
|
||||
foreach (Asset item in inventory.Where(static item => item.Tradable)) {
|
||||
(uint RealAppID, EAssetType Type, EAssetRarity Rarity) key = (item.RealAppID, item.Type, item.Rarity);
|
||||
|
||||
if (tradableState.TryGetValue(key, out Dictionary<ulong, uint>? tradableSet)) {
|
||||
tradableSet[item.ClassID] = tradableSet.GetValueOrDefault(item.ClassID) + item.Amount;
|
||||
} else {
|
||||
tradableState[key] = new Dictionary<ulong, uint> { { item.ClassID, item.Amount } };
|
||||
}
|
||||
}
|
||||
|
||||
return tradableState;
|
||||
}
|
||||
|
||||
internal static HashSet<Asset> GetTradableItemsFromInventory(IReadOnlyCollection<Asset> inventory, IReadOnlyDictionary<ulong, uint> classIDs, bool randomize = false) {
|
||||
if ((inventory == null) || (inventory.Count == 0)) {
|
||||
throw new ArgumentNullException(nameof(inventory));
|
||||
}
|
||||
|
||||
if ((classIDs == null) || (classIDs.Count == 0)) {
|
||||
throw new ArgumentNullException(nameof(classIDs));
|
||||
}
|
||||
|
||||
// We need a copy of classIDs passed since we're going to manipulate them
|
||||
Dictionary<ulong, uint> classIDsState = classIDs.ToDictionary();
|
||||
|
||||
HashSet<Asset> result = [];
|
||||
|
||||
IEnumerable<Asset> items = inventory.Where(static item => item.Tradable);
|
||||
|
||||
// Randomization helps to decrease "items no longer available" in regards to sending offers to other users
|
||||
if (randomize) {
|
||||
#pragma warning disable CA5394 // This call isn't used in a security-sensitive manner
|
||||
items = items.Where(item => classIDsState.ContainsKey(item.ClassID)).OrderBy(static _ => Random.Shared.Next());
|
||||
#pragma warning restore CA5394 // This call isn't used in a security-sensitive manner
|
||||
}
|
||||
|
||||
foreach (Asset item in items) {
|
||||
if (!classIDsState.TryGetValue(item.ClassID, out uint amount)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (amount >= item.Amount) {
|
||||
result.Add(item);
|
||||
|
||||
if (amount > item.Amount) {
|
||||
classIDsState[item.ClassID] = amount - item.Amount;
|
||||
} else {
|
||||
classIDsState.Remove(item.ClassID);
|
||||
|
||||
if (classIDsState.Count == 0) {
|
||||
return result;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
Asset itemToAdd = item.DeepClone();
|
||||
|
||||
itemToAdd.Amount = amount;
|
||||
|
||||
result.Add(itemToAdd);
|
||||
|
||||
classIDsState.Remove(itemToAdd.ClassID);
|
||||
|
||||
if (classIDsState.Count == 0) {
|
||||
return result;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// If we got here it means we still have classIDs to match
|
||||
throw new InvalidOperationException(nameof(classIDs));
|
||||
}
|
||||
|
||||
internal static bool IsEmptyForMatching(IReadOnlyDictionary<(uint RealAppID, EAssetType Type, EAssetRarity Rarity), Dictionary<ulong, uint>> fullState, IReadOnlyDictionary<(uint RealAppID, EAssetType Type, EAssetRarity Rarity), Dictionary<ulong, uint>> tradableState) {
|
||||
ArgumentNullException.ThrowIfNull(fullState);
|
||||
ArgumentNullException.ThrowIfNull(tradableState);
|
||||
|
||||
foreach (((uint RealAppID, EAssetType Type, EAssetRarity Rarity) set, IReadOnlyDictionary<ulong, uint> state) in tradableState) {
|
||||
if (!fullState.TryGetValue(set, out Dictionary<ulong, uint>? fullSet) || (fullSet.Count == 0)) {
|
||||
throw new InvalidOperationException(nameof(fullSet));
|
||||
}
|
||||
|
||||
if (!IsEmptyForMatching(fullSet, state)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// We didn't find any matchable combinations, so this inventory is empty
|
||||
return true;
|
||||
}
|
||||
|
||||
internal static bool IsEmptyForMatching(IReadOnlyDictionary<ulong, uint> fullSet, IReadOnlyDictionary<ulong, uint> tradableSet) {
|
||||
ArgumentNullException.ThrowIfNull(fullSet);
|
||||
ArgumentNullException.ThrowIfNull(tradableSet);
|
||||
|
||||
foreach ((ulong classID, uint amount) in tradableSet) {
|
||||
switch (amount) {
|
||||
case 0:
|
||||
// No tradable items, this should never happen, dictionary should not have this key to begin with
|
||||
throw new InvalidOperationException(nameof(amount));
|
||||
case 1:
|
||||
// Single tradable item, can be matchable or not depending on the rest of the inventory
|
||||
if (!fullSet.TryGetValue(classID, out uint fullAmount) || (fullAmount == 0)) {
|
||||
throw new InvalidOperationException(nameof(fullAmount));
|
||||
}
|
||||
|
||||
if (fullAmount > 1) {
|
||||
// If we have a single tradable item but more than 1 in total, this is matchable
|
||||
return false;
|
||||
}
|
||||
|
||||
// A single exclusive tradable item is not matchable, continue
|
||||
continue;
|
||||
default:
|
||||
// Any other combination of tradable items is always matchable
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// We didn't find any matchable combinations, so this inventory is empty
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -1,10 +1,12 @@
|
||||
// ----------------------------------------------------------------------------------------------
|
||||
// _ _ _ ____ _ _____
|
||||
// / \ _ __ ___ | |__ (_)/ ___| | |_ ___ __ _ _ __ ___ | ___|__ _ _ __ _ __ ___
|
||||
// / _ \ | '__|/ __|| '_ \ | |\___ \ | __|/ _ \ / _` || '_ ` _ \ | |_ / _` || '__|| '_ ` _ \
|
||||
// / ___ \ | | | (__ | | | || | ___) || |_| __/| (_| || | | | | || _|| (_| || | | | | | | |
|
||||
// /_/ \_\|_| \___||_| |_||_||____/ \__|\___| \__,_||_| |_| |_||_| \__,_||_| |_| |_| |_|
|
||||
// ----------------------------------------------------------------------------------------------
|
||||
// |
|
||||
// Copyright 2015-2023 Łukasz "JustArchi" Domeradzki
|
||||
// Copyright 2015-2024 Łukasz "JustArchi" Domeradzki
|
||||
// Contact: JustArchi@JustArchi.net
|
||||
// |
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
@@ -20,6 +22,7 @@
|
||||
// limitations under the License.
|
||||
|
||||
using System;
|
||||
using System.Collections.Frozen;
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.Immutable;
|
||||
using System.Globalization;
|
||||
@@ -27,6 +30,7 @@ using System.IO;
|
||||
using System.Linq;
|
||||
using System.Net;
|
||||
using System.Net.Http;
|
||||
using System.Text.Json;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using ArchiSteamFarm.Core;
|
||||
@@ -42,7 +46,6 @@ using ArchiSteamFarm.Steam.Storage;
|
||||
using ArchiSteamFarm.Storage;
|
||||
using ArchiSteamFarm.Web;
|
||||
using ArchiSteamFarm.Web.Responses;
|
||||
using Newtonsoft.Json.Linq;
|
||||
using SteamKit2;
|
||||
using SteamKit2.Internal;
|
||||
|
||||
@@ -51,6 +54,7 @@ namespace ArchiSteamFarm.OfficialPlugins.ItemsMatcher;
|
||||
internal sealed class RemoteCommunication : IAsyncDisposable, IDisposable {
|
||||
private const string MatchActivelyTradeOfferIDsStorageKey = $"{nameof(ItemsMatcher)}-{nameof(MatchActively)}-TradeOfferIDs";
|
||||
private const byte MaxAnnouncementTTL = 60; // Maximum amount of minutes we can wait if the next announcement doesn't happen naturally
|
||||
private const byte MaxInactivityDays = 14; // How long the server is willing to keep information about us for
|
||||
private const uint MaxItemsCount = 500000; // Server is unwilling to accept more items than this
|
||||
private const byte MaxTradeOffersActive = 5; // The actual upper limit is 30, but we should use lower amount to allow some bots to react before we hit the maximum allowed
|
||||
private const byte MinAnnouncementTTL = 5; // Minimum amount of minutes we must wait before the next Announcement
|
||||
@@ -59,12 +63,12 @@ internal sealed class RemoteCommunication : IAsyncDisposable, IDisposable {
|
||||
private const byte MinimumSteamGuardEnabledDays = 15; // As imposed by Steam limits
|
||||
private const byte MinPersonaStateTTL = 5; // Minimum amount of minutes we must wait before requesting persona state update
|
||||
|
||||
private static readonly ImmutableHashSet<Asset.EType> AcceptedMatchableTypes = ImmutableHashSet.Create(
|
||||
Asset.EType.Emoticon,
|
||||
Asset.EType.FoilTradingCard,
|
||||
Asset.EType.ProfileBackground,
|
||||
Asset.EType.TradingCard
|
||||
);
|
||||
private static readonly FrozenSet<EAssetType> AcceptedMatchableTypes = new HashSet<EAssetType>(4) {
|
||||
EAssetType.Emoticon,
|
||||
EAssetType.FoilTradingCard,
|
||||
EAssetType.ProfileBackground,
|
||||
EAssetType.TradingCard
|
||||
}.ToFrozenSet();
|
||||
|
||||
private readonly Bot Bot;
|
||||
private readonly Timer? HeartBeatTimer;
|
||||
@@ -227,7 +231,7 @@ internal sealed class RemoteCommunication : IAsyncDisposable, IDisposable {
|
||||
return;
|
||||
}
|
||||
|
||||
HashSet<Asset.EType> acceptedMatchableTypes = Bot.BotConfig.MatchableTypes.Where(AcceptedMatchableTypes.Contains).ToHashSet();
|
||||
HashSet<EAssetType> acceptedMatchableTypes = Bot.BotConfig.MatchableTypes.Where(AcceptedMatchableTypes.Contains).ToHashSet();
|
||||
|
||||
if (acceptedMatchableTypes.Count == 0) {
|
||||
throw new InvalidOperationException(nameof(acceptedMatchableTypes));
|
||||
@@ -248,7 +252,7 @@ internal sealed class RemoteCommunication : IAsyncDisposable, IDisposable {
|
||||
List<Asset> inventory;
|
||||
|
||||
try {
|
||||
inventory = await Bot.ArchiWebHandler.GetInventoryAsync().ToListAsync().ConfigureAwait(false);
|
||||
inventory = await Bot.ArchiHandler.GetMyInventoryAsync().ToListAsync().ConfigureAwait(false);
|
||||
} catch (HttpRequestException e) {
|
||||
// This is actually a network failure, so we'll stop sending heartbeats but not record it as valid check
|
||||
ShouldSendHeartBeats = false;
|
||||
@@ -278,12 +282,12 @@ internal sealed class RemoteCommunication : IAsyncDisposable, IDisposable {
|
||||
uint index = 0;
|
||||
ulong previousAssetID = 0;
|
||||
|
||||
List<AssetForListing> assetsForListing = new();
|
||||
List<AssetForListing> assetsForListing = [];
|
||||
|
||||
Dictionary<(uint RealAppID, Asset.EType Type, Asset.ERarity Rarity), bool> tradableSets = new();
|
||||
Dictionary<(uint RealAppID, EAssetType Type, EAssetRarity Rarity), bool> tradableSets = new();
|
||||
|
||||
foreach (Asset item in inventory) {
|
||||
if (item is { AssetID: > 0, Amount: > 0, ClassID: > 0, RealAppID: > 0, Type: > Asset.EType.Unknown, Rarity: > Asset.ERarity.Unknown, IsSteamPointsShopItem: false } && acceptedMatchableTypes.Contains(item.Type)) {
|
||||
if (item is { AssetID: > 0, Amount: > 0, ClassID: > 0, RealAppID: > 0, Type: > EAssetType.Unknown, Rarity: > EAssetRarity.Unknown, IsSteamPointsShopItem: false } && acceptedMatchableTypes.Contains(item.Type)) {
|
||||
// Only tradable assets matter for MatchEverything bots
|
||||
if (!matchEverything || item.Tradable) {
|
||||
assetsForListing.Add(new AssetForListing(item, index, previousAssetID));
|
||||
@@ -291,7 +295,7 @@ internal sealed class RemoteCommunication : IAsyncDisposable, IDisposable {
|
||||
|
||||
// But even for Fair bots, we should track and skip sets where we don't have any item to trade with
|
||||
if (!matchEverything) {
|
||||
(uint RealAppID, Asset.EType Type, Asset.ERarity Rarity) key = (item.RealAppID, item.Type, item.Rarity);
|
||||
(uint RealAppID, EAssetType Type, EAssetRarity Rarity) key = (item.RealAppID, item.Type, item.Rarity);
|
||||
|
||||
if (tradableSets.TryGetValue(key, out bool tradable)) {
|
||||
if (!tradable && item.Tradable) {
|
||||
@@ -332,7 +336,7 @@ internal sealed class RemoteCommunication : IAsyncDisposable, IDisposable {
|
||||
|
||||
string inventoryChecksumBeforeDeduplication = Backend.GenerateChecksumFor(assetsForListing);
|
||||
|
||||
if ((tradeToken == BotCache.LastAnnouncedTradeToken) && !string.IsNullOrEmpty(BotCache.LastInventoryChecksumBeforeDeduplication)) {
|
||||
if (BotCache.LastRequestAt.HasValue && (DateTime.UtcNow.Subtract(BotCache.LastRequestAt.Value).TotalDays < MaxInactivityDays) && (tradeToken == BotCache.LastAnnouncedTradeToken) && !string.IsNullOrEmpty(BotCache.LastInventoryChecksumBeforeDeduplication)) {
|
||||
if (inventoryChecksumBeforeDeduplication == BotCache.LastInventoryChecksumBeforeDeduplication) {
|
||||
// We've determined our state to be the same, we can skip announce entirely and start sending heartbeats exclusively
|
||||
bool triggerImmediately = !ShouldSendHeartBeats;
|
||||
@@ -372,16 +376,16 @@ internal sealed class RemoteCommunication : IAsyncDisposable, IDisposable {
|
||||
|
||||
if (!matchEverything) {
|
||||
// We should deduplicate our sets before sending them to the server, for doing that we'll use ASFB set parts data
|
||||
HashSet<uint> realAppIDs = new();
|
||||
Dictionary<(uint RealAppID, Asset.EType Type, Asset.ERarity Rarity), Dictionary<ulong, uint>> state = new();
|
||||
HashSet<uint> realAppIDs = [];
|
||||
Dictionary<(uint RealAppID, EAssetType Type, EAssetRarity Rarity), Dictionary<ulong, uint>> state = new();
|
||||
|
||||
foreach (AssetForListing asset in assetsForListing) {
|
||||
realAppIDs.Add(asset.RealAppID);
|
||||
|
||||
(uint RealAppID, Asset.EType Type, Asset.ERarity Rarity) key = (asset.RealAppID, asset.Type, asset.Rarity);
|
||||
(uint RealAppID, EAssetType Type, EAssetRarity Rarity) key = (asset.RealAppID, asset.Type, asset.Rarity);
|
||||
|
||||
if (state.TryGetValue(key, out Dictionary<ulong, uint>? set)) {
|
||||
set[asset.ClassID] = set.TryGetValue(asset.ClassID, out uint amount) ? amount + asset.Amount : asset.Amount;
|
||||
set[asset.ClassID] = set.GetValueOrDefault(asset.ClassID) + asset.Amount;
|
||||
} else {
|
||||
state[key] = new Dictionary<ulong, uint> { { asset.ClassID, asset.Amount } };
|
||||
}
|
||||
@@ -444,11 +448,11 @@ internal sealed class RemoteCommunication : IAsyncDisposable, IDisposable {
|
||||
return;
|
||||
}
|
||||
|
||||
Dictionary<(uint RealAppID, Asset.EType Type, Asset.ERarity Rarity), HashSet<ulong>> databaseSets = setPartsResponse.Content.Result.GroupBy(static setPart => (setPart.RealAppID, setPart.Type, setPart.Rarity)).ToDictionary(static group => group.Key, static group => group.Select(static setPart => setPart.ClassID).ToHashSet());
|
||||
Dictionary<(uint RealAppID, EAssetType Type, EAssetRarity Rarity), HashSet<ulong>> databaseSets = setPartsResponse.Content.Result.GroupBy(static setPart => (setPart.RealAppID, setPart.Type, setPart.Rarity)).ToDictionary(static group => group.Key, static group => group.Select(static setPart => setPart.ClassID).ToHashSet());
|
||||
|
||||
HashSet<(ulong ClassID, uint Amount)> setCopy = new();
|
||||
Dictionary<ulong, uint> setCopy = [];
|
||||
|
||||
foreach (((uint RealAppID, Asset.EType Type, Asset.ERarity Rarity) key, Dictionary<ulong, uint> set) in state) {
|
||||
foreach (((uint RealAppID, EAssetType Type, EAssetRarity Rarity) key, Dictionary<ulong, uint> set) in state) {
|
||||
if (!databaseSets.TryGetValue(key, out HashSet<ulong>? databaseSet)) {
|
||||
// We have no clue about this set, we can't do any optimization
|
||||
continue;
|
||||
@@ -469,7 +473,7 @@ internal sealed class RemoteCommunication : IAsyncDisposable, IDisposable {
|
||||
minimumAmount = amount;
|
||||
}
|
||||
|
||||
setCopy.Add((classID, amount));
|
||||
setCopy[classID] = amount;
|
||||
}
|
||||
|
||||
foreach ((ulong classID, uint amount) in setCopy) {
|
||||
@@ -483,10 +487,10 @@ internal sealed class RemoteCommunication : IAsyncDisposable, IDisposable {
|
||||
}
|
||||
}
|
||||
|
||||
HashSet<AssetForListing> assetsForListingFiltered = new();
|
||||
HashSet<AssetForListing> assetsForListingFiltered = [];
|
||||
|
||||
foreach (AssetForListing asset in assetsForListing.Where(asset => state.TryGetValue((asset.RealAppID, asset.Type, asset.Rarity), out Dictionary<ulong, uint>? setState) && setState.TryGetValue(asset.ClassID, out uint targetAmount) && (targetAmount > 0)).OrderByDescending(static asset => asset.Tradable).ThenByDescending(static asset => asset.Index)) {
|
||||
(uint RealAppID, Asset.EType Type, Asset.ERarity Rarity) key = (asset.RealAppID, asset.Type, asset.Rarity);
|
||||
(uint RealAppID, EAssetType Type, EAssetRarity Rarity) key = (asset.RealAppID, asset.Type, asset.Rarity);
|
||||
|
||||
if (!state.TryGetValue(key, out Dictionary<ulong, uint>? setState) || !setState.TryGetValue(asset.ClassID, out uint targetAmount) || (targetAmount == 0)) {
|
||||
// We're not interested in this combination
|
||||
@@ -536,7 +540,7 @@ internal sealed class RemoteCommunication : IAsyncDisposable, IDisposable {
|
||||
string checksum = Backend.GenerateChecksumFor(assetsForListing);
|
||||
string? previousChecksum = BotCache.LastAnnouncedAssetsForListing.Count > 0 ? Backend.GenerateChecksumFor(BotCache.LastAnnouncedAssetsForListing) : null;
|
||||
|
||||
if ((tradeToken == BotCache.LastAnnouncedTradeToken) && (checksum == previousChecksum)) {
|
||||
if (BotCache.LastRequestAt.HasValue && (DateTime.UtcNow.Subtract(BotCache.LastRequestAt.Value).TotalDays < MaxInactivityDays) && (tradeToken == BotCache.LastAnnouncedTradeToken) && (checksum == previousChecksum)) {
|
||||
// We've determined our state to be the same, we can skip announce entirely and start sending heartbeats exclusively
|
||||
bool triggerImmediately = !ShouldSendHeartBeats;
|
||||
|
||||
@@ -654,9 +658,11 @@ internal sealed class RemoteCommunication : IAsyncDisposable, IDisposable {
|
||||
LastAnnouncement = LastHeartBeat = DateTime.UtcNow;
|
||||
ShouldSendAnnouncementEarlier = false;
|
||||
ShouldSendHeartBeats = true;
|
||||
|
||||
BotCache.LastAnnouncedAssetsForListing.ReplaceWith(assetsForListing);
|
||||
BotCache.LastAnnouncedTradeToken = tradeToken;
|
||||
BotCache.LastInventoryChecksumBeforeDeduplication = inventoryChecksumBeforeDeduplication;
|
||||
BotCache.LastRequestAt = LastHeartBeat;
|
||||
|
||||
return;
|
||||
}
|
||||
@@ -757,9 +763,11 @@ internal sealed class RemoteCommunication : IAsyncDisposable, IDisposable {
|
||||
LastAnnouncement = LastHeartBeat = DateTime.UtcNow;
|
||||
ShouldSendAnnouncementEarlier = false;
|
||||
ShouldSendHeartBeats = true;
|
||||
|
||||
BotCache.LastAnnouncedAssetsForListing.ReplaceWith(assetsForListing);
|
||||
BotCache.LastAnnouncedTradeToken = tradeToken;
|
||||
BotCache.LastInventoryChecksumBeforeDeduplication = inventoryChecksumBeforeDeduplication;
|
||||
BotCache.LastRequestAt = LastHeartBeat;
|
||||
|
||||
return;
|
||||
}
|
||||
@@ -773,12 +781,12 @@ internal sealed class RemoteCommunication : IAsyncDisposable, IDisposable {
|
||||
|
||||
internal void TriggerMatchActivelyEarlier() {
|
||||
if (MatchActivelyTimer == null) {
|
||||
throw new InvalidOperationException(nameof(MatchActivelyTimer));
|
||||
}
|
||||
|
||||
// ReSharper disable once SuspiciousLockOverSynchronizationPrimitive - this is not a mistake, we need extra synchronization, and we can re-use the semaphore object for that
|
||||
lock (MatchActivelySemaphore) {
|
||||
MatchActivelyTimer.Change(TimeSpan.Zero, TimeSpan.FromHours(6));
|
||||
Utilities.InBackground(() => MatchActively());
|
||||
} else {
|
||||
// ReSharper disable once SuspiciousLockOverSynchronizationPrimitive - this is not a mistake, we need extra synchronization, and we can re-use the semaphore object for that
|
||||
lock (MatchActivelySemaphore) {
|
||||
MatchActivelyTimer.Change(TimeSpan.Zero, TimeSpan.FromHours(6));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -881,7 +889,7 @@ internal sealed class RemoteCommunication : IAsyncDisposable, IDisposable {
|
||||
throw new InvalidOperationException(nameof(ASF.GlobalConfig.LicenseID));
|
||||
}
|
||||
|
||||
if (!Bot.IsConnectedAndLoggedOn || Bot.BotConfig.TradingPreferences.HasFlag(BotConfig.ETradingPreferences.MatchEverything) || !Bot.BotConfig.TradingPreferences.HasFlag(BotConfig.ETradingPreferences.MatchActively)) {
|
||||
if (!Bot.IsConnectedAndLoggedOn || Bot.BotConfig.TradingPreferences.HasFlag(BotConfig.ETradingPreferences.MatchEverything)) {
|
||||
Bot.ArchiLogger.LogGenericTrace(Strings.ErrorAborted);
|
||||
|
||||
return;
|
||||
@@ -895,7 +903,7 @@ internal sealed class RemoteCommunication : IAsyncDisposable, IDisposable {
|
||||
return;
|
||||
}
|
||||
|
||||
HashSet<Asset.EType> acceptedMatchableTypes = Bot.BotConfig.MatchableTypes.Where(AcceptedMatchableTypes.Contains).ToHashSet();
|
||||
HashSet<EAssetType> acceptedMatchableTypes = Bot.BotConfig.MatchableTypes.Where(AcceptedMatchableTypes.Contains).ToHashSet();
|
||||
|
||||
if (acceptedMatchableTypes.Count == 0) {
|
||||
Bot.ArchiLogger.LogNullError(acceptedMatchableTypes);
|
||||
@@ -914,50 +922,215 @@ internal sealed class RemoteCommunication : IAsyncDisposable, IDisposable {
|
||||
try {
|
||||
Bot.ArchiLogger.LogGenericInfo(Strings.Starting);
|
||||
|
||||
Dictionary<ulong, Asset> ourInventory;
|
||||
HttpStatusCode? licenseStatus = await Backend.GetLicenseStatus(ASF.GlobalConfig.LicenseID.Value, WebBrowser).ConfigureAwait(false);
|
||||
|
||||
try {
|
||||
ourInventory = await Bot.ArchiWebHandler.GetInventoryAsync().Where(item => item is { AssetID: > 0, Amount: > 0, ClassID: > 0, RealAppID: > 0, Type: > Asset.EType.Unknown, Rarity: > Asset.ERarity.Unknown, IsSteamPointsShopItem: false } && acceptedMatchableTypes.Contains(item.Type) && !Bot.BotDatabase.MatchActivelyBlacklistAppIDs.Contains(item.RealAppID)).ToDictionaryAsync(static item => item.AssetID).ConfigureAwait(false);
|
||||
} catch (HttpRequestException e) {
|
||||
Bot.ArchiLogger.LogGenericWarningException(e);
|
||||
Bot.ArchiLogger.LogGenericWarning(string.Format(CultureInfo.CurrentCulture, Strings.WarningFailedWithError, nameof(ourInventory)));
|
||||
|
||||
return;
|
||||
} catch (Exception e) {
|
||||
Bot.ArchiLogger.LogGenericException(e);
|
||||
Bot.ArchiLogger.LogGenericWarning(string.Format(CultureInfo.CurrentCulture, Strings.WarningFailedWithError, nameof(ourInventory)));
|
||||
if (licenseStatus == null) {
|
||||
Bot.ArchiLogger.LogGenericWarning(string.Format(CultureInfo.CurrentCulture, Strings.WarningFailedWithError, nameof(licenseStatus)));
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if (ourInventory.Count == 0) {
|
||||
Bot.ArchiLogger.LogGenericInfo(string.Format(CultureInfo.CurrentCulture, Strings.ErrorIsEmpty, nameof(ourInventory)));
|
||||
if (!licenseStatus.Value.IsSuccessCode()) {
|
||||
Bot.ArchiLogger.LogGenericWarning(string.Format(CultureInfo.CurrentCulture, Strings.WarningFailedWithError, licenseStatus.Value));
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
HashSet<Asset> assetsForMatching;
|
||||
|
||||
try {
|
||||
assetsForMatching = await Bot.ArchiHandler.GetMyInventoryAsync().Where(item => item is { AssetID: > 0, Amount: > 0, ClassID: > 0, RealAppID: > 0, Type: > EAssetType.Unknown, Rarity: > EAssetRarity.Unknown, IsSteamPointsShopItem: false } && acceptedMatchableTypes.Contains(item.Type) && !Bot.BotDatabase.MatchActivelyBlacklistAppIDs.Contains(item.RealAppID)).ToHashSetAsync().ConfigureAwait(false);
|
||||
} catch (HttpRequestException e) {
|
||||
Bot.ArchiLogger.LogGenericWarningException(e);
|
||||
Bot.ArchiLogger.LogGenericWarning(string.Format(CultureInfo.CurrentCulture, Strings.WarningFailedWithError, nameof(assetsForMatching)));
|
||||
|
||||
return;
|
||||
} catch (Exception e) {
|
||||
Bot.ArchiLogger.LogGenericException(e);
|
||||
Bot.ArchiLogger.LogGenericWarning(string.Format(CultureInfo.CurrentCulture, Strings.WarningFailedWithError, nameof(assetsForMatching)));
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if (assetsForMatching.Count == 0) {
|
||||
Bot.ArchiLogger.LogGenericInfo(string.Format(CultureInfo.CurrentCulture, Strings.ErrorIsEmpty, nameof(assetsForMatching)));
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
// Remove from our inventory items that can't be possibly matched due to no dupes to offer available
|
||||
HashSet<(uint RealAppID, Asset.EType Type, Asset.ERarity Rarity)> setsToKeep = Trading.GetInventorySets(ourInventory.Values).Where(static set => set.Value.Any(static amount => amount > 1)).Select(static set => set.Key).ToHashSet();
|
||||
HashSet<(uint RealAppID, EAssetType Type, EAssetRarity Rarity)> setsToKeep = Trading.GetInventorySets(assetsForMatching).Where(static set => set.Value.Any(static amount => amount > 1)).Select(static set => set.Key).ToHashSet();
|
||||
|
||||
HashSet<ulong> assetIDsToRemove = ourInventory.Where(item => !setsToKeep.Contains((item.Value.RealAppID, item.Value.Type, item.Value.Rarity))).Select(static item => item.Key).ToHashSet();
|
||||
if (assetsForMatching.RemoveWhere(item => !setsToKeep.Contains((item.RealAppID, item.Type, item.Rarity))) > 0) {
|
||||
if (assetsForMatching.Count == 0) {
|
||||
Bot.ArchiLogger.LogGenericInfo(string.Format(CultureInfo.CurrentCulture, Strings.ErrorIsEmpty, nameof(assetsForMatching)));
|
||||
|
||||
foreach (ulong assetIDToRemove in assetIDsToRemove) {
|
||||
ourInventory.Remove(assetIDToRemove);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if (ourInventory.Count == 0) {
|
||||
Bot.ArchiLogger.LogGenericInfo(string.Format(CultureInfo.CurrentCulture, Strings.ErrorIsEmpty, nameof(ourInventory)));
|
||||
// We should deduplicate our sets before sending them to the server, for doing that we'll use ASFB set parts data
|
||||
HashSet<uint> realAppIDs = [];
|
||||
Dictionary<(uint RealAppID, EAssetType Type, EAssetRarity Rarity), Dictionary<ulong, uint>> setsState = new();
|
||||
|
||||
foreach (Asset asset in assetsForMatching) {
|
||||
realAppIDs.Add(asset.RealAppID);
|
||||
|
||||
(uint RealAppID, EAssetType Type, EAssetRarity Rarity) key = (asset.RealAppID, asset.Type, asset.Rarity);
|
||||
|
||||
if (setsState.TryGetValue(key, out Dictionary<ulong, uint>? set)) {
|
||||
set[asset.ClassID] = set.GetValueOrDefault(asset.ClassID) + asset.Amount;
|
||||
} else {
|
||||
setsState[key] = new Dictionary<ulong, uint> { { asset.ClassID, asset.Amount } };
|
||||
}
|
||||
}
|
||||
|
||||
if (!SignedInWithSteam) {
|
||||
HttpStatusCode? signInWithSteam = await ArchiNet.SignInWithSteam(Bot, WebBrowser).ConfigureAwait(false);
|
||||
|
||||
if ((signInWithSteam == null) || !signInWithSteam.Value.IsSuccessCode()) {
|
||||
// This is actually a network failure
|
||||
return;
|
||||
}
|
||||
|
||||
SignedInWithSteam = true;
|
||||
}
|
||||
|
||||
ObjectResponse<GenericResponse<ImmutableHashSet<SetPart>>>? setPartsResponse = await Backend.GetSetParts(WebBrowser, Bot.SteamID, acceptedMatchableTypes, realAppIDs).ConfigureAwait(false);
|
||||
|
||||
if (setPartsResponse == null) {
|
||||
// This is actually a network failure
|
||||
Bot.ArchiLogger.LogGenericWarning(string.Format(CultureInfo.CurrentCulture, Strings.ErrorObjectIsNull, nameof(setPartsResponse)));
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if (ourInventory.Count > MaxItemsCount) {
|
||||
Bot.ArchiLogger.LogGenericWarning(string.Format(CultureInfo.CurrentCulture, Strings.WarningFailedWithError, $"{nameof(ourInventory)} > {MaxItemsCount}"));
|
||||
if (setPartsResponse.StatusCode.IsRedirectionCode()) {
|
||||
Bot.ArchiLogger.LogGenericWarning(string.Format(CultureInfo.CurrentCulture, Strings.WarningFailedWithError, setPartsResponse.StatusCode));
|
||||
|
||||
if (setPartsResponse.FinalUri.Host != ArchiWebHandler.SteamCommunityURL.Host) {
|
||||
ASF.ArchiLogger.LogGenericError(string.Format(CultureInfo.CurrentCulture, Strings.WarningUnknownValuePleaseReport, nameof(setPartsResponse.FinalUri), setPartsResponse.FinalUri));
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
// We've expected the result, not the redirection to the sign in, we need to authenticate again
|
||||
SignedInWithSteam = false;
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
(HttpStatusCode StatusCode, ImmutableHashSet<ListedUser> Users)? response = await Backend.GetListedUsersForMatching(ASF.GlobalConfig.LicenseID.Value, Bot, WebBrowser, ourInventory.Values, acceptedMatchableTypes).ConfigureAwait(false);
|
||||
if (!setPartsResponse.StatusCode.IsSuccessCode()) {
|
||||
Bot.ArchiLogger.LogGenericWarning(string.Format(CultureInfo.CurrentCulture, Strings.WarningFailedWithError, setPartsResponse.StatusCode));
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if (setPartsResponse.Content?.Result == null) {
|
||||
// This should never happen if we got the correct response
|
||||
Bot.ArchiLogger.LogGenericError(string.Format(CultureInfo.CurrentCulture, Strings.WarningUnknownValuePleaseReport, nameof(setPartsResponse), setPartsResponse.Content?.Result));
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
Dictionary<(uint RealAppID, EAssetType Type, EAssetRarity Rarity), HashSet<ulong>> databaseSets = setPartsResponse.Content.Result.GroupBy(static setPart => (setPart.RealAppID, setPart.Type, setPart.Rarity)).ToDictionary(static group => group.Key, static group => group.Select(static setPart => setPart.ClassID).ToHashSet());
|
||||
|
||||
Dictionary<ulong, uint> setCopy = [];
|
||||
|
||||
foreach (((uint RealAppID, EAssetType Type, EAssetRarity Rarity) key, Dictionary<ulong, uint> set) in setsState) {
|
||||
uint minimumAmount = uint.MaxValue;
|
||||
uint maximumAmount = uint.MinValue;
|
||||
|
||||
foreach (uint amount in set.Values) {
|
||||
if (amount < minimumAmount) {
|
||||
minimumAmount = amount;
|
||||
}
|
||||
|
||||
if (amount > maximumAmount) {
|
||||
maximumAmount = amount;
|
||||
}
|
||||
}
|
||||
|
||||
if (maximumAmount < 2) {
|
||||
// We don't have anything to swap with, remove all entries from this set
|
||||
set.Clear();
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!databaseSets.TryGetValue(key, out HashSet<ulong>? databaseSet)) {
|
||||
// We have no clue about this set, we can't do any optimization
|
||||
continue;
|
||||
}
|
||||
|
||||
if ((databaseSet.Count != set.Count) || !databaseSet.SetEquals(set.Keys)) {
|
||||
// User either has more or less classIDs than we know about, we can't optimize this
|
||||
continue;
|
||||
}
|
||||
|
||||
if (maximumAmount - minimumAmount < 2) {
|
||||
// We don't have anything to swap with, remove all entries from this set
|
||||
set.Clear();
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
// User has all classIDs we know about, we can deduplicate his items based on lowest count
|
||||
setCopy.Clear();
|
||||
|
||||
foreach ((ulong classID, uint amount) in set) {
|
||||
setCopy[classID] = amount;
|
||||
}
|
||||
|
||||
foreach ((ulong classID, uint amount) in setCopy) {
|
||||
if (minimumAmount >= amount) {
|
||||
set.Remove(classID);
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
set[classID] = amount - minimumAmount;
|
||||
}
|
||||
}
|
||||
|
||||
HashSet<Asset> assetsForMatchingFiltered = [];
|
||||
|
||||
foreach (Asset asset in assetsForMatching.Where(asset => setsState.TryGetValue((asset.RealAppID, asset.Type, asset.Rarity), out Dictionary<ulong, uint>? setState) && setState.TryGetValue(asset.ClassID, out uint targetAmount) && (targetAmount > 0)).OrderByDescending(static asset => asset.Tradable)) {
|
||||
(uint RealAppID, EAssetType Type, EAssetRarity Rarity) key = (asset.RealAppID, asset.Type, asset.Rarity);
|
||||
|
||||
if (!setsState.TryGetValue(key, out Dictionary<ulong, uint>? setState) || !setState.TryGetValue(asset.ClassID, out uint targetAmount) || (targetAmount == 0)) {
|
||||
// We're not interested in this combination
|
||||
continue;
|
||||
}
|
||||
|
||||
if (asset.Amount >= targetAmount) {
|
||||
asset.Amount = targetAmount;
|
||||
|
||||
if (setState.Remove(asset.ClassID) && (setState.Count == 0)) {
|
||||
setsState.Remove(key);
|
||||
}
|
||||
} else {
|
||||
setState[asset.ClassID] = targetAmount - asset.Amount;
|
||||
}
|
||||
|
||||
assetsForMatchingFiltered.Add(asset);
|
||||
}
|
||||
|
||||
assetsForMatching = assetsForMatchingFiltered;
|
||||
|
||||
if (assetsForMatching.Count == 0) {
|
||||
Bot.ArchiLogger.LogGenericInfo(string.Format(CultureInfo.CurrentCulture, Strings.ErrorIsEmpty, nameof(assetsForMatching)));
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if (assetsForMatching.Count > MaxItemsCount) {
|
||||
Bot.ArchiLogger.LogGenericWarning(string.Format(CultureInfo.CurrentCulture, Strings.WarningFailedWithError, $"{nameof(assetsForMatching)} > {MaxItemsCount}"));
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
(HttpStatusCode StatusCode, ImmutableHashSet<ListedUser> Users)? response = await Backend.GetListedUsersForMatching(ASF.GlobalConfig.LicenseID.Value, Bot, WebBrowser, assetsForMatching, acceptedMatchableTypes).ConfigureAwait(false);
|
||||
|
||||
if (response == null) {
|
||||
Bot.ArchiLogger.LogGenericWarning(string.Format(CultureInfo.CurrentCulture, Strings.WarningFailedWithError, nameof(response)));
|
||||
@@ -980,9 +1153,7 @@ internal sealed class RemoteCommunication : IAsyncDisposable, IDisposable {
|
||||
#pragma warning disable CA2000 // False positive, we're actually wrapping it in the using clause below exactly for that purpose
|
||||
using (await Bot.Actions.GetTradingLock().ConfigureAwait(false)) {
|
||||
#pragma warning restore CA2000 // False positive, we're actually wrapping it in the using clause below exactly for that purpose
|
||||
Bot.ArchiLogger.LogGenericInfo(Strings.Starting);
|
||||
|
||||
tradesSent = await MatchActively(response.Value.Users, ourInventory, acceptedMatchableTypes).ConfigureAwait(false);
|
||||
tradesSent = await MatchActively(response.Value.Users, assetsForMatching, acceptedMatchableTypes).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
Bot.ArchiLogger.LogGenericInfo(Strings.Done);
|
||||
@@ -996,22 +1167,22 @@ internal sealed class RemoteCommunication : IAsyncDisposable, IDisposable {
|
||||
}
|
||||
}
|
||||
|
||||
private async Task<bool> MatchActively(IReadOnlyCollection<ListedUser> listedUsers, Dictionary<ulong, Asset> ourInventory, IReadOnlyCollection<Asset.EType> acceptedMatchableTypes) {
|
||||
private async Task<bool> MatchActively(IReadOnlyCollection<ListedUser> listedUsers, IReadOnlyCollection<Asset> ourAssets, IReadOnlyCollection<EAssetType> acceptedMatchableTypes) {
|
||||
if ((listedUsers == null) || (listedUsers.Count == 0)) {
|
||||
throw new ArgumentNullException(nameof(listedUsers));
|
||||
}
|
||||
|
||||
if ((ourInventory == null) || (ourInventory.Count == 0)) {
|
||||
throw new ArgumentNullException(nameof(ourInventory));
|
||||
if ((ourAssets == null) || (ourAssets.Count == 0)) {
|
||||
throw new ArgumentNullException(nameof(ourAssets));
|
||||
}
|
||||
|
||||
if ((acceptedMatchableTypes == null) || (acceptedMatchableTypes.Count == 0)) {
|
||||
throw new ArgumentNullException(nameof(acceptedMatchableTypes));
|
||||
}
|
||||
|
||||
(Dictionary<(uint RealAppID, Asset.EType Type, Asset.ERarity Rarity), Dictionary<ulong, uint>> ourFullState, Dictionary<(uint RealAppID, Asset.EType Type, Asset.ERarity Rarity), Dictionary<ulong, uint>> ourTradableState) = Trading.GetDividedInventoryState(ourInventory.Values);
|
||||
(Dictionary<(uint RealAppID, EAssetType Type, EAssetRarity Rarity), Dictionary<ulong, uint>> ourFullState, Dictionary<(uint RealAppID, EAssetType Type, EAssetRarity Rarity), Dictionary<ulong, uint>> ourTradableState) = MatchingUtilities.GetDividedInventoryState(ourAssets);
|
||||
|
||||
if (Trading.IsEmptyForMatching(ourFullState, ourTradableState)) {
|
||||
if (MatchingUtilities.IsEmptyForMatching(ourFullState, ourTradableState)) {
|
||||
// User doesn't have any more dupes in the inventory
|
||||
Bot.ArchiLogger.LogGenericTrace(string.Format(CultureInfo.CurrentCulture, Strings.ErrorIsEmpty, $"{nameof(ourFullState)} || {nameof(ourTradableState)}"));
|
||||
|
||||
@@ -1021,26 +1192,34 @@ internal sealed class RemoteCommunication : IAsyncDisposable, IDisposable {
|
||||
// Cancel previous trade offers sent and deprioritize SteamIDs that didn't answer us in this round
|
||||
HashSet<ulong>? matchActivelyTradeOfferIDs = null;
|
||||
|
||||
JToken? matchActivelyTradeOfferIDsToken = Bot.BotDatabase.LoadFromJsonStorage(MatchActivelyTradeOfferIDsStorageKey);
|
||||
JsonElement matchActivelyTradeOfferIDsToken = Bot.BotDatabase.LoadFromJsonStorage(MatchActivelyTradeOfferIDsStorageKey);
|
||||
|
||||
if (matchActivelyTradeOfferIDsToken != null) {
|
||||
if (matchActivelyTradeOfferIDsToken.ValueKind == JsonValueKind.Array) {
|
||||
try {
|
||||
matchActivelyTradeOfferIDs = matchActivelyTradeOfferIDsToken.ToObject<HashSet<ulong>>();
|
||||
matchActivelyTradeOfferIDs = new HashSet<ulong>(matchActivelyTradeOfferIDsToken.GetArrayLength());
|
||||
|
||||
foreach (JsonElement tradeIDElement in matchActivelyTradeOfferIDsToken.EnumerateArray()) {
|
||||
if (!tradeIDElement.TryGetUInt64(out ulong tradeID)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
matchActivelyTradeOfferIDs.Add(tradeID);
|
||||
}
|
||||
} catch (Exception e) {
|
||||
Bot.ArchiLogger.LogGenericWarningException(e);
|
||||
}
|
||||
}
|
||||
|
||||
matchActivelyTradeOfferIDs ??= new HashSet<ulong>();
|
||||
matchActivelyTradeOfferIDs ??= [];
|
||||
|
||||
HashSet<ulong> deprioritizedSteamIDs = new();
|
||||
HashSet<ulong> deprioritizedSteamIDs = [];
|
||||
|
||||
if (matchActivelyTradeOfferIDs.Count > 0) {
|
||||
// This is not a mandatory step, we allow it to fail
|
||||
HashSet<TradeOffer>? sentTradeOffers = await Bot.ArchiWebHandler.GetTradeOffers(true, false, true, false).ConfigureAwait(false);
|
||||
|
||||
if (sentTradeOffers != null) {
|
||||
HashSet<ulong> activeTradeOfferIDs = new();
|
||||
HashSet<ulong> activeTradeOfferIDs = [];
|
||||
|
||||
foreach (TradeOffer tradeOffer in sentTradeOffers.Where(tradeOffer => (tradeOffer.State == ETradeOfferState.Active) && matchActivelyTradeOfferIDs.Contains(tradeOffer.TradeOfferID))) {
|
||||
deprioritizedSteamIDs.Add(tradeOffer.OtherSteamID64);
|
||||
@@ -1054,7 +1233,7 @@ internal sealed class RemoteCommunication : IAsyncDisposable, IDisposable {
|
||||
matchActivelyTradeOfferIDs = activeTradeOfferIDs;
|
||||
|
||||
if (matchActivelyTradeOfferIDs.Count > 0) {
|
||||
Bot.BotDatabase.SaveToJsonStorage(MatchActivelyTradeOfferIDsStorageKey, JToken.FromObject(matchActivelyTradeOfferIDs));
|
||||
Bot.BotDatabase.SaveToJsonStorage(MatchActivelyTradeOfferIDsStorageKey, matchActivelyTradeOfferIDs);
|
||||
} else {
|
||||
Bot.BotDatabase.DeleteFromJsonStorage(MatchActivelyTradeOfferIDsStorageKey);
|
||||
}
|
||||
@@ -1062,14 +1241,24 @@ internal sealed class RemoteCommunication : IAsyncDisposable, IDisposable {
|
||||
}
|
||||
}
|
||||
|
||||
HashSet<ulong> pendingMobileTradeOfferIDs = new();
|
||||
Dictionary<ulong, Asset> ourInventory = ourAssets.ToDictionary(static asset => asset.AssetID);
|
||||
|
||||
HashSet<ulong> pendingMobileTradeOfferIDs = [];
|
||||
|
||||
byte maxTradeHoldDuration = ASF.GlobalConfig?.MaxTradeHoldDuration ?? GlobalConfig.DefaultMaxTradeHoldDuration;
|
||||
|
||||
byte failuresInRow = 0;
|
||||
uint matchedSets = 0;
|
||||
|
||||
foreach (ListedUser listedUser in listedUsers.Where(listedUser => (listedUser.SteamID != Bot.SteamID) && acceptedMatchableTypes.Any(listedUser.MatchableTypes.Contains) && !Bot.IsBlacklistedFromTrades(listedUser.SteamID)).OrderBy(listedUser => deprioritizedSteamIDs.Contains(listedUser.SteamID)).ThenByDescending(static listedUser => listedUser.MatchEverything).ThenBy(static listedUser => listedUser.TotalInventoryCount)) {
|
||||
HashSet<(uint RealAppID, EAssetType Type, EAssetRarity Rarity)> skippedSetsThisUser = [];
|
||||
HashSet<(uint RealAppID, EAssetType Type, EAssetRarity Rarity)> skippedSetsThisTrade = [];
|
||||
|
||||
Dictionary<ulong, uint> classIDsToGive = new();
|
||||
Dictionary<ulong, uint> classIDsToReceive = new();
|
||||
Dictionary<ulong, uint> fairClassIDsToGive = new();
|
||||
Dictionary<ulong, uint> fairClassIDsToReceive = new();
|
||||
|
||||
foreach (ListedUser listedUser in listedUsers.Where(listedUser => (listedUser.SteamID != Bot.SteamID) && acceptedMatchableTypes.Any(listedUser.MatchableTypes.Contains) && !Bot.IsBlacklistedFromTrades(listedUser.SteamID)).OrderByDescending(listedUser => !deprioritizedSteamIDs.Contains(listedUser.SteamID)).ThenByDescending(static listedUser => listedUser.TotalGamesCount > 1).ThenByDescending(static listedUser => listedUser.MatchEverything).ThenBy(static listedUser => listedUser.TotalInventoryCount)) {
|
||||
if (failuresInRow >= WebBrowser.MaxTries) {
|
||||
Bot.ArchiLogger.LogGenericWarning(string.Format(CultureInfo.CurrentCulture, Strings.WarningFailedWithError, $"{nameof(failuresInRow)} >= {WebBrowser.MaxTries}"));
|
||||
|
||||
@@ -1082,7 +1271,7 @@ internal sealed class RemoteCommunication : IAsyncDisposable, IDisposable {
|
||||
break;
|
||||
}
|
||||
|
||||
HashSet<(uint RealAppID, Asset.EType Type, Asset.ERarity Rarity)> wantedSets = ourTradableState.Keys.Where(set => listedUser.MatchableTypes.Contains(set.Type)).ToHashSet();
|
||||
HashSet<(uint RealAppID, EAssetType Type, EAssetRarity Rarity)> wantedSets = ourTradableState.Keys.Where(set => listedUser.MatchableTypes.Contains(set.Type)).ToHashSet();
|
||||
|
||||
if (wantedSets.Count == 0) {
|
||||
continue;
|
||||
@@ -1103,26 +1292,27 @@ internal sealed class RemoteCommunication : IAsyncDisposable, IDisposable {
|
||||
continue;
|
||||
}
|
||||
|
||||
HashSet<Asset> theirInventory = listedUser.Assets.Where(item => (!listedUser.MatchEverything || item.Tradable) && wantedSets.Contains((item.RealAppID, item.Type, item.Rarity)) && ((tradeHoldDuration.Value == 0) || !(item.Type is Asset.EType.FoilTradingCard or Asset.EType.TradingCard && CardsFarmer.SalesBlacklist.Contains(item.RealAppID)))).Select(static asset => asset.ToAsset()).ToHashSet();
|
||||
HashSet<Asset> theirInventory = listedUser.Assets.Where(item => (!listedUser.MatchEverything || item.Tradable) && wantedSets.Contains((item.RealAppID, item.Type, item.Rarity)) && ((tradeHoldDuration.Value == 0) || !(item.Type is EAssetType.FoilTradingCard or EAssetType.TradingCard && CardsFarmer.SalesBlacklist.Contains(item.RealAppID)))).Select(static asset => asset.ToAsset()).ToHashSet();
|
||||
|
||||
if (theirInventory.Count == 0) {
|
||||
continue;
|
||||
}
|
||||
|
||||
HashSet<(uint RealAppID, Asset.EType Type, Asset.ERarity Rarity)> skippedSetsThisUser = new();
|
||||
skippedSetsThisUser.Clear();
|
||||
|
||||
Dictionary<(uint RealAppID, Asset.EType Type, Asset.ERarity Rarity), Dictionary<ulong, uint>> theirTradableState = Trading.GetTradableInventoryState(theirInventory);
|
||||
Dictionary<(uint RealAppID, EAssetType Type, EAssetRarity Rarity), Dictionary<ulong, uint>> theirTradableState = MatchingUtilities.GetTradableInventoryState(theirInventory);
|
||||
|
||||
for (byte i = 0; i < Trading.MaxTradesPerAccount; i++) {
|
||||
byte itemsInTrade = 0;
|
||||
HashSet<(uint RealAppID, Asset.EType Type, Asset.ERarity Rarity)> skippedSetsThisTrade = new();
|
||||
|
||||
Dictionary<ulong, uint> classIDsToGive = new();
|
||||
Dictionary<ulong, uint> classIDsToReceive = new();
|
||||
Dictionary<ulong, uint> fairClassIDsToGive = new();
|
||||
Dictionary<ulong, uint> fairClassIDsToReceive = new();
|
||||
skippedSetsThisTrade.Clear();
|
||||
|
||||
foreach (((uint RealAppID, Asset.EType Type, Asset.ERarity Rarity) set, Dictionary<ulong, uint> ourFullItems) in ourFullState.Where(set => !skippedSetsThisUser.Contains(set.Key) && listedUser.MatchableTypes.Contains(set.Key.Type) && set.Value.Values.Any(static count => count > 1))) {
|
||||
classIDsToGive.Clear();
|
||||
classIDsToReceive.Clear();
|
||||
fairClassIDsToGive.Clear();
|
||||
fairClassIDsToReceive.Clear();
|
||||
|
||||
foreach (((uint RealAppID, EAssetType Type, EAssetRarity Rarity) set, Dictionary<ulong, uint> ourFullItems) in ourFullState.Where(set => !skippedSetsThisUser.Contains(set.Key) && listedUser.MatchableTypes.Contains(set.Key.Type) && set.Value.Values.Any(static count => count > 1))) {
|
||||
if (!ourTradableState.TryGetValue(set, out Dictionary<ulong, uint>? ourTradableItems) || (ourTradableItems.Count == 0)) {
|
||||
// We may have no more tradable items from this set
|
||||
continue;
|
||||
@@ -1133,14 +1323,14 @@ internal sealed class RemoteCommunication : IAsyncDisposable, IDisposable {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (Trading.IsEmptyForMatching(ourFullItems, ourTradableItems)) {
|
||||
if (MatchingUtilities.IsEmptyForMatching(ourFullItems, ourTradableItems)) {
|
||||
// We may have no more matchable items from this set
|
||||
continue;
|
||||
}
|
||||
|
||||
// Those 2 collections are on user-basis since we can't be sure that the trade passes through (and therefore we need to keep original state in case of a failure)
|
||||
Dictionary<ulong, uint> ourFullSet = new(ourFullItems);
|
||||
Dictionary<ulong, uint> ourTradableSet = new(ourTradableItems);
|
||||
Dictionary<ulong, uint> ourFullSet = ourFullItems.ToDictionary();
|
||||
Dictionary<ulong, uint> ourTradableSet = ourTradableItems.ToDictionary();
|
||||
|
||||
bool match;
|
||||
|
||||
@@ -1152,26 +1342,27 @@ internal sealed class RemoteCommunication : IAsyncDisposable, IDisposable {
|
||||
continue;
|
||||
}
|
||||
|
||||
foreach ((ulong theirItem, uint theirTradableAmount) in theirTradableItems.OrderBy(item => ourFullSet.TryGetValue(item.Key, out uint ourAmountOfTheirItem) ? ourAmountOfTheirItem : 0)) {
|
||||
foreach ((ulong theirItem, uint theirTradableAmount) in theirTradableItems.OrderBy(item => ourFullSet.GetValueOrDefault(item.Key))) {
|
||||
if (ourFullSet.TryGetValue(theirItem, out uint ourAmountOfTheirItem) && (ourFullAmount <= ourAmountOfTheirItem + 1)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!listedUser.MatchEverything) {
|
||||
// We have a potential match, let's check fairness for them
|
||||
fairClassIDsToGive.TryGetValue(ourItem, out uint fairGivenAmount);
|
||||
fairClassIDsToReceive.TryGetValue(theirItem, out uint fairReceivedAmount);
|
||||
uint fairGivenAmount = fairClassIDsToGive.GetValueOrDefault(ourItem);
|
||||
uint fairReceivedAmount = fairClassIDsToReceive.GetValueOrDefault(theirItem);
|
||||
|
||||
fairClassIDsToGive[ourItem] = ++fairGivenAmount;
|
||||
fairClassIDsToReceive[theirItem] = ++fairReceivedAmount;
|
||||
|
||||
// Filter their inventory for the sets we're trading or have traded with this user
|
||||
HashSet<Asset> fairFiltered = theirInventory.Where(item => ((item.RealAppID == set.RealAppID) && (item.Type == set.Type) && (item.Rarity == set.Rarity)) || skippedSetsThisTrade.Contains((item.RealAppID, item.Type, item.Rarity))).Select(static item => item.CreateShallowCopy()).ToHashSet();
|
||||
HashSet<Asset> fairFiltered = theirInventory.Where(item => ((item.RealAppID == set.RealAppID) && (item.Type == set.Type) && (item.Rarity == set.Rarity)) || skippedSetsThisTrade.Contains((item.RealAppID, item.Type, item.Rarity))).ToHashSet();
|
||||
|
||||
// Copy list to HashSet<Steam.Asset>
|
||||
HashSet<Asset> fairItemsToGive = Trading.GetTradableItemsFromInventory(ourInventory.Values.Where(item => ((item.RealAppID == set.RealAppID) && (item.Type == set.Type) && (item.Rarity == set.Rarity)) || skippedSetsThisTrade.Contains((item.RealAppID, item.Type, item.Rarity))).Select(static item => item.CreateShallowCopy()).ToHashSet(), fairClassIDsToGive.ToDictionary(static classID => classID.Key, static classID => classID.Value));
|
||||
HashSet<Asset> fairItemsToReceive = Trading.GetTradableItemsFromInventory(fairFiltered.Select(static item => item.CreateShallowCopy()).ToHashSet(), fairClassIDsToReceive.ToDictionary(static classID => classID.Key, static classID => classID.Value));
|
||||
// Get tradable items from our and their inventory
|
||||
HashSet<Asset> fairItemsToGive = MatchingUtilities.GetTradableItemsFromInventory(ourInventory.Values.Where(item => ((item.RealAppID == set.RealAppID) && (item.Type == set.Type) && (item.Rarity == set.Rarity)) || skippedSetsThisTrade.Contains((item.RealAppID, item.Type, item.Rarity))).ToHashSet(), fairClassIDsToGive);
|
||||
HashSet<Asset> fairItemsToReceive = MatchingUtilities.GetTradableItemsFromInventory(fairFiltered, fairClassIDsToReceive);
|
||||
|
||||
// Actual check
|
||||
// Actual check, since we do this against remote user, we flip places for items
|
||||
if (!Trading.IsTradeNeutralOrBetter(fairFiltered, fairItemsToReceive, fairItemsToGive)) {
|
||||
// Revert the changes
|
||||
if (fairGivenAmount > 1) {
|
||||
@@ -1194,11 +1385,11 @@ internal sealed class RemoteCommunication : IAsyncDisposable, IDisposable {
|
||||
skippedSetsThisTrade.Add(set);
|
||||
|
||||
// Update our state based on given items
|
||||
classIDsToGive[ourItem] = classIDsToGive.TryGetValue(ourItem, out uint ourGivenAmount) ? ourGivenAmount + 1 : 1;
|
||||
classIDsToGive[ourItem] = classIDsToGive.GetValueOrDefault(ourItem) + 1;
|
||||
ourFullSet[ourItem] = ourFullAmount - 1; // We don't need to remove anything here because we can guarantee that ourItem.Value is at least 2
|
||||
|
||||
// Update our state based on received items
|
||||
classIDsToReceive[theirItem] = classIDsToReceive.TryGetValue(theirItem, out uint ourReceivedAmount) ? ourReceivedAmount + 1 : 1;
|
||||
classIDsToReceive[theirItem] = classIDsToReceive.GetValueOrDefault(theirItem) + 1;
|
||||
ourFullSet[theirItem] = ourAmountOfTheirItem + 1;
|
||||
|
||||
if (ourTradableAmount > 1) {
|
||||
@@ -1239,8 +1430,8 @@ internal sealed class RemoteCommunication : IAsyncDisposable, IDisposable {
|
||||
}
|
||||
|
||||
// Remove the items from inventories
|
||||
HashSet<Asset> itemsToGive = Trading.GetTradableItemsFromInventory(ourInventory.Values, classIDsToGive);
|
||||
HashSet<Asset> itemsToReceive = Trading.GetTradableItemsFromInventory(theirInventory, classIDsToReceive, true);
|
||||
HashSet<Asset> itemsToGive = MatchingUtilities.GetTradableItemsFromInventory(ourInventory.Values, classIDsToGive);
|
||||
HashSet<Asset> itemsToReceive = MatchingUtilities.GetTradableItemsFromInventory(theirInventory, classIDsToReceive, true);
|
||||
|
||||
if ((itemsToGive.Count != itemsToReceive.Count) || !Trading.IsFairExchange(itemsToGive, itemsToReceive)) {
|
||||
// Failsafe
|
||||
@@ -1256,7 +1447,7 @@ internal sealed class RemoteCommunication : IAsyncDisposable, IDisposable {
|
||||
if (tradeOfferIDs?.Count > 0) {
|
||||
matchActivelyTradeOfferIDs.UnionWith(tradeOfferIDs);
|
||||
|
||||
Bot.BotDatabase.SaveToJsonStorage(MatchActivelyTradeOfferIDsStorageKey, JToken.FromObject(matchActivelyTradeOfferIDs));
|
||||
Bot.BotDatabase.SaveToJsonStorage(MatchActivelyTradeOfferIDsStorageKey, matchActivelyTradeOfferIDs);
|
||||
}
|
||||
|
||||
if (mobileTradeOfferIDs?.Count > 0) {
|
||||
@@ -1324,10 +1515,14 @@ internal sealed class RemoteCommunication : IAsyncDisposable, IDisposable {
|
||||
// However, since this is only an assumption, we must mark newly acquired items as untradable so we're sure that they're not considered for trading, only for matching
|
||||
foreach (Asset itemToReceive in itemsToReceive) {
|
||||
if (ourInventory.TryGetValue(itemToReceive.AssetID, out Asset? item)) {
|
||||
item.Tradable = false;
|
||||
item.Description ??= new InventoryDescription(itemToReceive.AppID, itemToReceive.ClassID, itemToReceive.InstanceID, realAppID: itemToReceive.RealAppID, type: itemToReceive.Type, rarity: itemToReceive.Rarity);
|
||||
|
||||
item.Description.Body.tradable = false;
|
||||
item.Amount += itemToReceive.Amount;
|
||||
} else {
|
||||
itemToReceive.Tradable = false;
|
||||
itemToReceive.Description ??= new InventoryDescription(itemToReceive.AppID, itemToReceive.ClassID, itemToReceive.InstanceID, realAppID: itemToReceive.RealAppID, type: itemToReceive.Type, rarity: itemToReceive.Rarity);
|
||||
|
||||
itemToReceive.Description.Body.tradable = false;
|
||||
ourInventory[itemToReceive.AssetID] = itemToReceive;
|
||||
}
|
||||
|
||||
@@ -1336,11 +1531,7 @@ internal sealed class RemoteCommunication : IAsyncDisposable, IDisposable {
|
||||
throw new InvalidOperationException(nameof(fullAmounts));
|
||||
}
|
||||
|
||||
if (!fullAmounts.TryGetValue(itemToReceive.ClassID, out uint fullAmount)) {
|
||||
fullAmount = 0;
|
||||
}
|
||||
|
||||
fullAmounts[itemToReceive.ClassID] = itemToReceive.Amount + fullAmount;
|
||||
fullAmounts[itemToReceive.ClassID] = fullAmounts.GetValueOrDefault(itemToReceive.ClassID) + itemToReceive.Amount;
|
||||
}
|
||||
|
||||
skippedSetsThisUser.UnionWith(skippedSetsThisTrade);
|
||||
@@ -1352,7 +1543,7 @@ internal sealed class RemoteCommunication : IAsyncDisposable, IDisposable {
|
||||
|
||||
matchedSets += (uint) skippedSetsThisUser.Count;
|
||||
|
||||
if (Trading.IsEmptyForMatching(ourFullState, ourTradableState)) {
|
||||
if (MatchingUtilities.IsEmptyForMatching(ourFullState, ourTradableState)) {
|
||||
// User doesn't have any more dupes in the inventory
|
||||
break;
|
||||
}
|
||||
@@ -1436,15 +1627,42 @@ internal sealed class RemoteCommunication : IAsyncDisposable, IDisposable {
|
||||
return;
|
||||
}
|
||||
|
||||
if (response.StatusCode.IsClientErrorCode()) {
|
||||
BotCache ??= await BotCache.CreateOrLoad(BotCacheFilePath).ConfigureAwait(false);
|
||||
|
||||
if (!response.StatusCode.IsSuccessCode()) {
|
||||
ShouldSendHeartBeats = false;
|
||||
|
||||
Bot.ArchiLogger.LogGenericWarning(string.Format(CultureInfo.CurrentCulture, Strings.WarningFailedWithError, response.StatusCode));
|
||||
|
||||
return;
|
||||
switch (response.StatusCode) {
|
||||
case HttpStatusCode.Conflict:
|
||||
// ArchiNet told us to that we need to announce again
|
||||
LastAnnouncement = DateTime.MinValue;
|
||||
|
||||
BotCache.LastAnnouncedAssetsForListing.Clear();
|
||||
BotCache.LastInventoryChecksumBeforeDeduplication = BotCache.LastAnnouncedTradeToken = null;
|
||||
BotCache.LastRequestAt = null;
|
||||
|
||||
return;
|
||||
case HttpStatusCode.Forbidden:
|
||||
// ArchiNet told us to stop submitting data for now
|
||||
LastAnnouncement = DateTime.UtcNow.AddYears(1);
|
||||
|
||||
return;
|
||||
case HttpStatusCode.TooManyRequests:
|
||||
// ArchiNet told us to try again later
|
||||
LastAnnouncement = DateTime.UtcNow.AddDays(1);
|
||||
|
||||
return;
|
||||
default:
|
||||
// There is something wrong with our payload or the server, we shouldn't retry for at least several hours
|
||||
LastAnnouncement = DateTime.UtcNow.AddHours(6);
|
||||
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
LastHeartBeat = DateTime.UtcNow;
|
||||
BotCache.LastRequestAt = LastHeartBeat = DateTime.UtcNow;
|
||||
} finally {
|
||||
RequestsSemaphore.Release();
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
// ----------------------------------------------------------------------------------------------
|
||||
// _ _ _ ____ _ _____
|
||||
// / \ _ __ ___ | |__ (_)/ ___| | |_ ___ __ _ _ __ ___ | ___|__ _ _ __ _ __ ___
|
||||
// / _ \ | '__|/ __|| '_ \ | |\___ \ | __|/ _ \ / _` || '_ ` _ \ | |_ / _` || '__|| '_ ` _ \
|
||||
// / ___ \ | | | (__ | | | || | ___) || |_| __/| (_| || | | | | || _|| (_| || | | | | | | |
|
||||
// /_/ \_\|_| \___||_| |_||_||____/ \__|\___| \__,_||_| |_| |_||_| \__,_||_| |_| |_| |_|
|
||||
// ----------------------------------------------------------------------------------------------
|
||||
// |
|
||||
// Copyright 2015-2023 Łukasz "JustArchi" Domeradzki
|
||||
// Copyright 2015-2024 Łukasz "JustArchi" Domeradzki
|
||||
// Contact: JustArchi@JustArchi.net
|
||||
// |
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
// ----------------------------------------------------------------------------------------------
|
||||
// _ _ _ ____ _ _____
|
||||
// / \ _ __ ___ | |__ (_)/ ___| | |_ ___ __ _ _ __ ___ | ___|__ _ _ __ _ __ ___
|
||||
// / _ \ | '__|/ __|| '_ \ | |\___ \ | __|/ _ \ / _` || '_ ` _ \ | |_ / _` || '__|| '_ ` _ \
|
||||
// / ___ \ | | | (__ | | | || | ___) || |_| __/| (_| || | | | | || _|| (_| || | | | | | | |
|
||||
// /_/ \_\|_| \___||_| |_||_||____/ \__|\___| \__,_||_| |_| |_||_| \__,_||_| |_| |_| |_|
|
||||
// ----------------------------------------------------------------------------------------------
|
||||
// |
|
||||
// Copyright 2015-2023 Łukasz "JustArchi" Domeradzki
|
||||
// Copyright 2015-2024 Łukasz "JustArchi" Domeradzki
|
||||
// Contact: JustArchi@JustArchi.net
|
||||
// |
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
@@ -27,17 +29,15 @@ using System.IO;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using ArchiSteamFarm.Core;
|
||||
using ArchiSteamFarm.Helpers.Json;
|
||||
using ArchiSteamFarm.Localization;
|
||||
using ArchiSteamFarm.Steam;
|
||||
using Newtonsoft.Json;
|
||||
using SteamKit2;
|
||||
using SteamKit2.Internal;
|
||||
|
||||
namespace ArchiSteamFarm.OfficialPlugins.MobileAuthenticator;
|
||||
|
||||
internal static class Commands {
|
||||
private const byte MaxFinalizationAttempts = 900 / Steam.Security.MobileAuthenticator.CodeInterval;
|
||||
|
||||
internal static async Task<string?> OnBotCommand(Bot bot, EAccess access, string message, string[] args, ulong steamID = 0) {
|
||||
ArgumentNullException.ThrowIfNull(bot);
|
||||
|
||||
@@ -128,7 +128,7 @@ internal static class Commands {
|
||||
return bot.Commands.FormatBotResponse(string.Format(CultureInfo.CurrentCulture, Strings.ErrorIsEmpty, nameof(json)));
|
||||
}
|
||||
|
||||
Steam.Security.MobileAuthenticator? mobileAuthenticator = JsonConvert.DeserializeObject<Steam.Security.MobileAuthenticator>(json);
|
||||
Steam.Security.MobileAuthenticator? mobileAuthenticator = json.ToJsonObject<Steam.Security.MobileAuthenticator>();
|
||||
|
||||
if (mobileAuthenticator == null) {
|
||||
return bot.Commands.FormatBotResponse(string.Format(CultureInfo.CurrentCulture, Strings.ErrorIsEmpty, nameof(json)));
|
||||
@@ -144,43 +144,22 @@ internal static class Commands {
|
||||
|
||||
ulong steamTime = await mobileAuthenticator.GetSteamTime().ConfigureAwait(false);
|
||||
|
||||
bool successFinalizing = false;
|
||||
string? code = mobileAuthenticator.GenerateTokenForTime(steamTime);
|
||||
|
||||
for (byte i = 0; i < MaxFinalizationAttempts; i++) {
|
||||
if (i > 0) {
|
||||
steamTime += Steam.Security.MobileAuthenticator.CodeInterval;
|
||||
}
|
||||
|
||||
string? code = mobileAuthenticator.GenerateTokenForTime(steamTime);
|
||||
|
||||
if (string.IsNullOrEmpty(code)) {
|
||||
return bot.Commands.FormatBotResponse(string.Format(CultureInfo.CurrentCulture, Strings.WarningFailedWithError, nameof(mobileAuthenticator.GenerateTokenForTime)));
|
||||
}
|
||||
|
||||
CTwoFactor_FinalizeAddAuthenticator_Response? response = await mobileAuthenticatorHandler.FinalizeAuthenticator(bot.SteamID, activationCode, code, steamTime).ConfigureAwait(false);
|
||||
|
||||
if (response == null) {
|
||||
return bot.Commands.FormatBotResponse(string.Format(CultureInfo.CurrentCulture, Strings.WarningFailedWithError, nameof(mobileAuthenticatorHandler.FinalizeAuthenticator)));
|
||||
}
|
||||
|
||||
if (response.want_more) {
|
||||
// OK, whatever
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!response.success) {
|
||||
EResult result = (EResult) response.status;
|
||||
|
||||
return bot.Commands.FormatBotResponse(string.Format(CultureInfo.CurrentCulture, Strings.WarningFailedWithError, result));
|
||||
}
|
||||
|
||||
successFinalizing = true;
|
||||
|
||||
break;
|
||||
if (string.IsNullOrEmpty(code)) {
|
||||
return bot.Commands.FormatBotResponse(string.Format(CultureInfo.CurrentCulture, Strings.WarningFailedWithError, nameof(mobileAuthenticator.GenerateTokenForTime)));
|
||||
}
|
||||
|
||||
if (!successFinalizing) {
|
||||
return bot.Commands.FormatBotResponse(string.Format(CultureInfo.CurrentCulture, Strings.ErrorRequestFailedTooManyTimes, MaxFinalizationAttempts));
|
||||
CTwoFactor_FinalizeAddAuthenticator_Response? response = await mobileAuthenticatorHandler.FinalizeAuthenticator(bot.SteamID, activationCode, code, steamTime).ConfigureAwait(false);
|
||||
|
||||
if (response == null) {
|
||||
return bot.Commands.FormatBotResponse(string.Format(CultureInfo.CurrentCulture, Strings.WarningFailedWithError, nameof(mobileAuthenticatorHandler.FinalizeAuthenticator)));
|
||||
}
|
||||
|
||||
if (!response.success) {
|
||||
EResult result = (EResult) response.status;
|
||||
|
||||
return bot.Commands.FormatBotResponse(string.Format(CultureInfo.CurrentCulture, Strings.WarningFailedWithError, result));
|
||||
}
|
||||
|
||||
if (!bot.TryImportAuthenticator(mobileAuthenticator)) {
|
||||
@@ -220,7 +199,7 @@ internal static class Commands {
|
||||
|
||||
IList<string?> results = await Utilities.InParallel(bots.Select(bot => ResponseTwoFactorFinalize(Steam.Interaction.Commands.GetProxyAccess(bot, access, steamID), bot, activationCode))).ConfigureAwait(false);
|
||||
|
||||
List<string> responses = new(results.Where(static result => !string.IsNullOrEmpty(result))!);
|
||||
List<string> responses = [..results.Where(static result => !string.IsNullOrEmpty(result))];
|
||||
|
||||
return responses.Count > 0 ? string.Join(Environment.NewLine, responses) : null;
|
||||
}
|
||||
@@ -261,7 +240,7 @@ internal static class Commands {
|
||||
return bot.Commands.FormatBotResponse(string.Format(CultureInfo.CurrentCulture, Strings.ErrorIsEmpty, nameof(json)));
|
||||
}
|
||||
|
||||
Steam.Security.MobileAuthenticator? mobileAuthenticator = JsonConvert.DeserializeObject<Steam.Security.MobileAuthenticator>(json);
|
||||
Steam.Security.MobileAuthenticator? mobileAuthenticator = json.ToJsonObject<Steam.Security.MobileAuthenticator>();
|
||||
|
||||
if (mobileAuthenticator == null) {
|
||||
return bot.Commands.FormatBotResponse(string.Format(CultureInfo.CurrentCulture, Strings.ErrorIsEmpty, nameof(json)));
|
||||
@@ -313,7 +292,7 @@ internal static class Commands {
|
||||
|
||||
IList<string?> results = await Utilities.InParallel(bots.Select(bot => ResponseTwoFactorFinalized(Steam.Interaction.Commands.GetProxyAccess(bot, access, steamID), bot, activationCode))).ConfigureAwait(false);
|
||||
|
||||
List<string> responses = new(results.Where(static result => !string.IsNullOrEmpty(result))!);
|
||||
List<string> responses = [..results.Where(static result => !string.IsNullOrEmpty(result))];
|
||||
|
||||
return responses.Count > 0 ? string.Join(Environment.NewLine, responses) : null;
|
||||
}
|
||||
@@ -360,7 +339,7 @@ internal static class Commands {
|
||||
MaFileData maFileData = new(response, bot.SteamID, deviceID);
|
||||
|
||||
string maFilePendingPath = $"{bot.GetFilePath(Bot.EFileType.MobileAuthenticator)}.PENDING";
|
||||
string json = JsonConvert.SerializeObject(maFileData, Formatting.Indented);
|
||||
string json = maFileData.ToJsonText(true);
|
||||
|
||||
try {
|
||||
await File.WriteAllTextAsync(maFilePendingPath, json).ConfigureAwait(false);
|
||||
@@ -392,7 +371,7 @@ internal static class Commands {
|
||||
|
||||
IList<string?> results = await Utilities.InParallel(bots.Select(bot => ResponseTwoFactorInit(Steam.Interaction.Commands.GetProxyAccess(bot, access, steamID), bot))).ConfigureAwait(false);
|
||||
|
||||
List<string> responses = new(results.Where(static result => !string.IsNullOrEmpty(result))!);
|
||||
List<string> responses = [..results.Where(static result => !string.IsNullOrEmpty(result))];
|
||||
|
||||
return responses.Count > 0 ? string.Join(Environment.NewLine, responses) : null;
|
||||
}
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
// ----------------------------------------------------------------------------------------------
|
||||
// _ _ _ ____ _ _____
|
||||
// / \ _ __ ___ | |__ (_)/ ___| | |_ ___ __ _ _ __ ___ | ___|__ _ _ __ _ __ ___
|
||||
// / _ \ | '__|/ __|| '_ \ | |\___ \ | __|/ _ \ / _` || '_ ` _ \ | |_ / _` || '__|| '_ ` _ \
|
||||
// / ___ \ | | | (__ | | | || | ___) || |_| __/| (_| || | | | | || _|| (_| || | | | | | | |
|
||||
// /_/ \_\|_| \___||_| |_||_||____/ \__|\___| \__,_||_| |_| |_||_| \__,_||_| |_| |_| |_|
|
||||
// ----------------------------------------------------------------------------------------------
|
||||
// |
|
||||
// Copyright 2015-2023 Łukasz "JustArchi" Domeradzki
|
||||
// Copyright 2015-2024 Łukasz "JustArchi" Domeradzki
|
||||
// Contact: JustArchi@JustArchi.net
|
||||
// |
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
@@ -20,48 +22,71 @@
|
||||
// limitations under the License.
|
||||
|
||||
using System;
|
||||
using Newtonsoft.Json;
|
||||
using System.Text.Json.Serialization;
|
||||
using SteamKit2;
|
||||
using SteamKit2.Internal;
|
||||
|
||||
namespace ArchiSteamFarm.OfficialPlugins.MobileAuthenticator;
|
||||
|
||||
internal sealed class MaFileData {
|
||||
[JsonProperty("account_name", Required = Required.Always)]
|
||||
internal readonly string AccountName;
|
||||
[JsonInclude]
|
||||
[JsonPropertyName("account_name")]
|
||||
[JsonRequired]
|
||||
internal string AccountName { get; private init; }
|
||||
|
||||
[JsonProperty("device_id", Required = Required.Always)]
|
||||
internal readonly string DeviceID;
|
||||
[JsonInclude]
|
||||
[JsonPropertyName("device_id")]
|
||||
[JsonRequired]
|
||||
internal string DeviceID { get; private init; }
|
||||
|
||||
[JsonProperty("identity_secret", Required = Required.Always)]
|
||||
internal readonly string IdentitySecret;
|
||||
[JsonInclude]
|
||||
[JsonPropertyName("identity_secret")]
|
||||
[JsonRequired]
|
||||
internal string IdentitySecret { get; private init; }
|
||||
|
||||
[JsonProperty("revocation_code", Required = Required.Always)]
|
||||
internal readonly string RevocationCode;
|
||||
[JsonInclude]
|
||||
[JsonPropertyName("revocation_code")]
|
||||
[JsonRequired]
|
||||
internal string RevocationCode { get; private init; }
|
||||
|
||||
[JsonProperty("secret_1", Required = Required.Always)]
|
||||
internal readonly string Secret1;
|
||||
[JsonInclude]
|
||||
[JsonPropertyName("secret_1")]
|
||||
[JsonRequired]
|
||||
internal string Secret1 { get; private init; }
|
||||
|
||||
[JsonProperty("serial_number", Required = Required.Always)]
|
||||
internal readonly ulong SerialNumber;
|
||||
[JsonInclude]
|
||||
[JsonPropertyName("serial_number")]
|
||||
[JsonRequired]
|
||||
internal ulong SerialNumber { get; private init; }
|
||||
|
||||
[JsonProperty("server_time", Required = Required.Always)]
|
||||
internal readonly ulong ServerTime;
|
||||
[JsonInclude]
|
||||
[JsonPropertyName("server_time")]
|
||||
[JsonRequired]
|
||||
internal ulong ServerTime { get; private init; }
|
||||
|
||||
[JsonProperty(Required = Required.Always)]
|
||||
internal readonly MaFileSessionData Session;
|
||||
[JsonInclude]
|
||||
[JsonRequired]
|
||||
internal MaFileSessionData Session { get; private init; }
|
||||
|
||||
[JsonProperty("shared_secret", Required = Required.Always)]
|
||||
internal readonly string SharedSecret;
|
||||
[JsonInclude]
|
||||
[JsonPropertyName("shared_secret")]
|
||||
[JsonRequired]
|
||||
internal string SharedSecret { get; private init; }
|
||||
|
||||
[JsonProperty("status", Required = Required.Always)]
|
||||
internal readonly int Status;
|
||||
[JsonInclude]
|
||||
[JsonPropertyName("status")]
|
||||
[JsonRequired]
|
||||
internal int Status { get; private init; }
|
||||
|
||||
[JsonProperty("token_gid", Required = Required.Always)]
|
||||
internal readonly string TokenGid;
|
||||
[JsonInclude]
|
||||
[JsonPropertyName("token_gid")]
|
||||
[JsonRequired]
|
||||
internal string TokenGid { get; private init; }
|
||||
|
||||
[JsonProperty("uri", Required = Required.Always)]
|
||||
internal readonly string Uri;
|
||||
[JsonInclude]
|
||||
[JsonPropertyName("uri")]
|
||||
[JsonRequired]
|
||||
internal string Uri { get; private init; }
|
||||
|
||||
internal MaFileData(CTwoFactor_AddAuthenticator_Response data, ulong steamID, string deviceID) {
|
||||
ArgumentNullException.ThrowIfNull(data);
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
// ----------------------------------------------------------------------------------------------
|
||||
// _ _ _ ____ _ _____
|
||||
// / \ _ __ ___ | |__ (_)/ ___| | |_ ___ __ _ _ __ ___ | ___|__ _ _ __ _ __ ___
|
||||
// / _ \ | '__|/ __|| '_ \ | |\___ \ | __|/ _ \ / _` || '_ ` _ \ | |_ / _` || '__|| '_ ` _ \
|
||||
// / ___ \ | | | (__ | | | || | ___) || |_| __/| (_| || | | | | || _|| (_| || | | | | | | |
|
||||
// /_/ \_\|_| \___||_| |_||_||____/ \__|\___| \__,_||_| |_| |_||_| \__,_||_| |_| |_| |_|
|
||||
// ----------------------------------------------------------------------------------------------
|
||||
// |
|
||||
// Copyright 2015-2023 Łukasz "JustArchi" Domeradzki
|
||||
// Copyright 2015-2024 Łukasz "JustArchi" Domeradzki
|
||||
// Contact: JustArchi@JustArchi.net
|
||||
// |
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
@@ -20,14 +22,15 @@
|
||||
// limitations under the License.
|
||||
|
||||
using System;
|
||||
using Newtonsoft.Json;
|
||||
using System.Text.Json.Serialization;
|
||||
using SteamKit2;
|
||||
|
||||
namespace ArchiSteamFarm.OfficialPlugins.MobileAuthenticator;
|
||||
|
||||
internal sealed class MaFileSessionData {
|
||||
[JsonProperty(Required = Required.Always)]
|
||||
internal readonly ulong SteamID;
|
||||
[JsonInclude]
|
||||
[JsonRequired]
|
||||
internal ulong SteamID { get; private init; }
|
||||
|
||||
internal MaFileSessionData(ulong steamID) {
|
||||
if ((steamID == 0) || !new SteamID(steamID).IsIndividualAccount) {
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
// ----------------------------------------------------------------------------------------------
|
||||
// _ _ _ ____ _ _____
|
||||
// / \ _ __ ___ | |__ (_)/ ___| | |_ ___ __ _ _ __ ___ | ___|__ _ _ __ _ __ ___
|
||||
// / _ \ | '__|/ __|| '_ \ | |\___ \ | __|/ _ \ / _` || '_ ` _ \ | |_ / _` || '__|| '_ ` _ \
|
||||
// / ___ \ | | | (__ | | | || | ___) || |_| __/| (_| || | | | | || _|| (_| || | | | | | | |
|
||||
// /_/ \_\|_| \___||_| |_||_||____/ \__|\___| \__,_||_| |_| |_||_| \__,_||_| |_| |_| |_|
|
||||
// ----------------------------------------------------------------------------------------------
|
||||
// |
|
||||
// Copyright 2015-2023 Łukasz "JustArchi" Domeradzki
|
||||
// Copyright 2015-2024 Łukasz "JustArchi" Domeradzki
|
||||
// Contact: JustArchi@JustArchi.net
|
||||
// |
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
@@ -61,7 +63,6 @@ internal sealed class MobileAuthenticatorHandler : ClientMsgHandler {
|
||||
authenticator_type = 1,
|
||||
authenticator_time = Utilities.GetUnixTime(),
|
||||
device_identifier = deviceID,
|
||||
sms_phone_id = "1",
|
||||
steamid = steamID
|
||||
};
|
||||
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
// ----------------------------------------------------------------------------------------------
|
||||
// _ _ _ ____ _ _____
|
||||
// / \ _ __ ___ | |__ (_)/ ___| | |_ ___ __ _ _ __ ___ | ___|__ _ _ __ _ __ ___
|
||||
// / _ \ | '__|/ __|| '_ \ | |\___ \ | __|/ _ \ / _` || '_ ` _ \ | |_ / _` || '__|| '_ ` _ \
|
||||
// / ___ \ | | | (__ | | | || | ___) || |_| __/| (_| || | | | | || _|| (_| || | | | | | | |
|
||||
// /_/ \_\|_| \___||_| |_||_||____/ \__|\___| \__,_||_| |_| |_||_| \__,_||_| |_| |_| |_|
|
||||
// ----------------------------------------------------------------------------------------------
|
||||
// |
|
||||
// Copyright 2015-2023 Łukasz "JustArchi" Domeradzki
|
||||
// Copyright 2015-2024 Łukasz "JustArchi" Domeradzki
|
||||
// Contact: JustArchi@JustArchi.net
|
||||
// |
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
@@ -22,15 +24,16 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.ComponentModel;
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using System.Composition;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.Text.Json.Serialization;
|
||||
using System.Threading.Tasks;
|
||||
using ArchiSteamFarm.Core;
|
||||
using ArchiSteamFarm.OfficialPlugins.MobileAuthenticator.Localization;
|
||||
using ArchiSteamFarm.Plugins;
|
||||
using ArchiSteamFarm.Plugins.Interfaces;
|
||||
using ArchiSteamFarm.Steam;
|
||||
using Newtonsoft.Json;
|
||||
using SteamKit2;
|
||||
|
||||
namespace ArchiSteamFarm.OfficialPlugins.MobileAuthenticator;
|
||||
@@ -38,10 +41,12 @@ namespace ArchiSteamFarm.OfficialPlugins.MobileAuthenticator;
|
||||
[Export(typeof(IPlugin))]
|
||||
[SuppressMessage("ReSharper", "MemberCanBeFileLocal")]
|
||||
internal sealed class MobileAuthenticatorPlugin : OfficialPlugin, IBotCommand2, IBotSteamClient {
|
||||
[JsonProperty]
|
||||
[JsonInclude]
|
||||
[Required]
|
||||
public override string Name => nameof(MobileAuthenticatorPlugin);
|
||||
|
||||
[JsonProperty]
|
||||
[JsonInclude]
|
||||
[Required]
|
||||
public override Version Version => typeof(MobileAuthenticatorPlugin).Assembly.GetName().Version ?? throw new InvalidOperationException(nameof(Version));
|
||||
|
||||
public async Task<string?> OnBotCommand(Bot bot, EAccess access, string message, string[] args, ulong steamID = 0) {
|
||||
|
||||
@@ -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" />
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
// ----------------------------------------------------------------------------------------------
|
||||
// _ _ _ ____ _ _____
|
||||
// / \ _ __ ___ | |__ (_)/ ___| | |_ ___ __ _ _ __ ___ | ___|__ _ _ __ _ __ ___
|
||||
// / _ \ | '__|/ __|| '_ \ | |\___ \ | __|/ _ \ / _` || '_ ` _ \ | |_ / _` || '__|| '_ ` _ \
|
||||
// / ___ \ | | | (__ | | | || | ___) || |_| __/| (_| || | | | | || _|| (_| || | | | | | | |
|
||||
// /_/ \_\|_| \___||_| |_||_||____/ \__|\___| \__,_||_| |_| |_||_| \__,_||_| |_| |_| |_|
|
||||
// ----------------------------------------------------------------------------------------------
|
||||
// |
|
||||
// Copyright 2015-2023 Łukasz "JustArchi" Domeradzki
|
||||
// Copyright 2015-2024 Łukasz "JustArchi" Domeradzki
|
||||
// Contact: JustArchi@JustArchi.net
|
||||
// |
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
// ----------------------------------------------------------------------------------------------
|
||||
// _ _ _ ____ _ _____
|
||||
// / \ _ __ ___ | |__ (_)/ ___| | |_ ___ __ _ _ __ ___ | ___|__ _ _ __ _ __ ___
|
||||
// / _ \ | '__|/ __|| '_ \ | |\___ \ | __|/ _ \ / _` || '_ ` _ \ | |_ / _` || '__|| '_ ` _ \
|
||||
// / ___ \ | | | (__ | | | || | ___) || |_| __/| (_| || | | | | || _|| (_| || | | | | | | |
|
||||
// /_/ \_\|_| \___||_| |_||_||____/ \__|\___| \__,_||_| |_| |_||_| \__,_||_| |_| |_| |_|
|
||||
// ----------------------------------------------------------------------------------------------
|
||||
// |
|
||||
// Copyright 2015-2023 Łukasz "JustArchi" Domeradzki
|
||||
// Copyright 2015-2024 Łukasz "JustArchi" Domeradzki
|
||||
// Contact: JustArchi@JustArchi.net
|
||||
// |
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
@@ -23,36 +25,52 @@ using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.Immutable;
|
||||
using System.Globalization;
|
||||
using System.Text.Json.Serialization;
|
||||
using ArchiSteamFarm.Core;
|
||||
using Newtonsoft.Json;
|
||||
using SteamKit2;
|
||||
|
||||
namespace ArchiSteamFarm.OfficialPlugins.SteamTokenDumper.Data;
|
||||
|
||||
internal sealed class SubmitRequest {
|
||||
[JsonProperty("guid", Required = Required.Always)]
|
||||
private static string Guid => ASF.GlobalDatabase?.Identifier.ToString("N") ?? throw new InvalidOperationException(nameof(ASF.GlobalDatabase.Identifier));
|
||||
|
||||
[JsonProperty("token", Required = Required.Always)]
|
||||
private static string Token => SharedInfo.Token;
|
||||
|
||||
[JsonProperty("v", Required = Required.Always)]
|
||||
private static byte Version => SharedInfo.ApiVersion;
|
||||
|
||||
[JsonProperty("apps", Required = Required.Always)]
|
||||
private readonly ImmutableDictionary<string, string> Apps;
|
||||
|
||||
[JsonProperty("depots", Required = Required.Always)]
|
||||
private readonly ImmutableDictionary<string, string> Depots;
|
||||
|
||||
private readonly ulong SteamID;
|
||||
|
||||
[JsonProperty("subs", Required = Required.Always)]
|
||||
private readonly ImmutableDictionary<string, string> Subs;
|
||||
#pragma warning disable CA1822 // We can't make it static, STJ doesn't serialize it otherwise
|
||||
[JsonInclude]
|
||||
[JsonPropertyName("guid")]
|
||||
private string Guid => ASF.GlobalDatabase?.Identifier.ToString("N") ?? throw new InvalidOperationException(nameof(ASF.GlobalDatabase.Identifier));
|
||||
#pragma warning restore CA1822 // We can't make it static, STJ doesn't serialize it otherwise
|
||||
|
||||
[JsonProperty("steamid", Required = Required.Always)]
|
||||
[JsonInclude]
|
||||
[JsonPropertyName("steamid")]
|
||||
private string SteamIDText => new SteamID(SteamID).Render();
|
||||
|
||||
#pragma warning disable CA1822 // We can't make it static, STJ doesn't serialize it otherwise
|
||||
[JsonInclude]
|
||||
[JsonPropertyName("token")]
|
||||
private string Token => SharedInfo.Token;
|
||||
#pragma warning restore CA1822 // We can't make it static, STJ doesn't serialize it otherwise
|
||||
|
||||
#pragma warning disable CA1822 // We can't make it static, STJ doesn't serialize it otherwise
|
||||
[JsonInclude]
|
||||
[JsonPropertyName("v")]
|
||||
private byte Version => SharedInfo.ApiVersion;
|
||||
#pragma warning restore CA1822 // We can't make it static, STJ doesn't serialize it otherwise
|
||||
|
||||
[JsonInclude]
|
||||
[JsonPropertyName("apps")]
|
||||
[JsonRequired]
|
||||
private ImmutableDictionary<string, string> Apps { get; init; }
|
||||
|
||||
[JsonInclude]
|
||||
[JsonPropertyName("depots")]
|
||||
[JsonRequired]
|
||||
private ImmutableDictionary<string, string> Depots { get; init; }
|
||||
|
||||
[JsonInclude]
|
||||
[JsonPropertyName("subs")]
|
||||
[JsonRequired]
|
||||
private ImmutableDictionary<string, string> Subs { get; init; }
|
||||
|
||||
internal SubmitRequest(ulong steamID, IReadOnlyCollection<KeyValuePair<uint, ulong>> apps, IReadOnlyCollection<KeyValuePair<uint, ulong>> accessTokens, IReadOnlyCollection<KeyValuePair<uint, string>> depots) {
|
||||
if ((steamID == 0) || !new SteamID(steamID).IsIndividualAccount) {
|
||||
throw new ArgumentOutOfRangeException(nameof(steamID));
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
// ----------------------------------------------------------------------------------------------
|
||||
// _ _ _ ____ _ _____
|
||||
// / \ _ __ ___ | |__ (_)/ ___| | |_ ___ __ _ _ __ ___ | ___|__ _ _ __ _ __ ___
|
||||
// / _ \ | '__|/ __|| '_ \ | |\___ \ | __|/ _ \ / _` || '_ ` _ \ | |_ / _` || '__|| '_ ` _ \
|
||||
// / ___ \ | | | (__ | | | || | ___) || |_| __/| (_| || | | | | || _|| (_| || | | | | | | |
|
||||
// /_/ \_\|_| \___||_| |_||_||____/ \__|\___| \__,_||_| |_| |_||_| \__,_||_| |_| |_| |_|
|
||||
// ----------------------------------------------------------------------------------------------
|
||||
// |
|
||||
// Copyright 2015-2023 Łukasz "JustArchi" Domeradzki
|
||||
// Copyright 2015-2024 Łukasz "JustArchi" Domeradzki
|
||||
// Contact: JustArchi@JustArchi.net
|
||||
// |
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
@@ -20,22 +22,21 @@
|
||||
// limitations under the License.
|
||||
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using Newtonsoft.Json;
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace ArchiSteamFarm.OfficialPlugins.SteamTokenDumper.Data;
|
||||
|
||||
#pragma warning disable CA1812 // False positive, the class is used during json deserialization
|
||||
[SuppressMessage("ReSharper", "ClassCannotBeInstantiated")]
|
||||
internal sealed class SubmitResponse {
|
||||
#pragma warning disable CS0649 // False positive, the field is used during json deserialization
|
||||
[JsonProperty("data", Required = Required.DisallowNull)]
|
||||
internal readonly SubmitResponseData? Data;
|
||||
#pragma warning restore CS0649 // False positive, the field is used during json deserialization
|
||||
[JsonInclude]
|
||||
[JsonPropertyName("data")]
|
||||
internal SubmitResponseData? Data { get; private init; }
|
||||
|
||||
#pragma warning disable CS0649 // False positive, the field is used during json deserialization
|
||||
[JsonProperty("success", Required = Required.Always)]
|
||||
internal readonly bool Success;
|
||||
#pragma warning restore CS0649 // False positive, the field is used during json deserialization
|
||||
[JsonInclude]
|
||||
[JsonPropertyName("success")]
|
||||
[JsonRequired]
|
||||
internal bool Success { get; private init; }
|
||||
|
||||
[JsonConstructor]
|
||||
private SubmitResponse() { }
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
// ----------------------------------------------------------------------------------------------
|
||||
// _ _ _ ____ _ _____
|
||||
// / \ _ __ ___ | |__ (_)/ ___| | |_ ___ __ _ _ __ ___ | ___|__ _ _ __ _ __ ___
|
||||
// / _ \ | '__|/ __|| '_ \ | |\___ \ | __|/ _ \ / _` || '_ ` _ \ | |_ / _` || '__|| '_ ` _ \
|
||||
// / ___ \ | | | (__ | | | || | ___) || |_| __/| (_| || | | | | || _|| (_| || | | | | | | |
|
||||
// /_/ \_\|_| \___||_| |_||_||____/ \__|\___| \__,_||_| |_| |_||_| \__,_||_| |_| |_| |_|
|
||||
// ----------------------------------------------------------------------------------------------
|
||||
// |
|
||||
// Copyright 2015-2023 Łukasz "JustArchi" Domeradzki
|
||||
// Copyright 2015-2024 Łukasz "JustArchi" Domeradzki
|
||||
// Contact: JustArchi@JustArchi.net
|
||||
// |
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
@@ -20,28 +22,40 @@
|
||||
// limitations under the License.
|
||||
|
||||
using System.Collections.Immutable;
|
||||
using Newtonsoft.Json;
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace ArchiSteamFarm.OfficialPlugins.SteamTokenDumper.Data;
|
||||
|
||||
#pragma warning disable CA1812 // False positive, the class is used during json deserialization
|
||||
internal sealed class SubmitResponseData {
|
||||
[JsonProperty("new_apps", Required = Required.Always)]
|
||||
internal readonly ImmutableHashSet<uint> NewApps = ImmutableHashSet<uint>.Empty;
|
||||
[JsonInclude]
|
||||
[JsonPropertyName("new_apps")]
|
||||
[JsonRequired]
|
||||
internal ImmutableHashSet<uint> NewApps { get; private init; } = [];
|
||||
|
||||
[JsonProperty("new_depots", Required = Required.Always)]
|
||||
internal readonly ImmutableHashSet<uint> NewDepots = ImmutableHashSet<uint>.Empty;
|
||||
[JsonInclude]
|
||||
[JsonPropertyName("new_depots")]
|
||||
[JsonRequired]
|
||||
internal ImmutableHashSet<uint> NewDepots { get; private init; } = [];
|
||||
|
||||
[JsonProperty("new_subs", Required = Required.Always)]
|
||||
internal readonly ImmutableHashSet<uint> NewPackages = ImmutableHashSet<uint>.Empty;
|
||||
[JsonInclude]
|
||||
[JsonPropertyName("new_subs")]
|
||||
[JsonRequired]
|
||||
internal ImmutableHashSet<uint> NewPackages { get; private init; } = [];
|
||||
|
||||
[JsonProperty("verified_apps", Required = Required.Always)]
|
||||
internal readonly ImmutableHashSet<uint> VerifiedApps = ImmutableHashSet<uint>.Empty;
|
||||
[JsonInclude]
|
||||
[JsonPropertyName("verified_apps")]
|
||||
[JsonRequired]
|
||||
internal ImmutableHashSet<uint> VerifiedApps { get; private init; } = [];
|
||||
|
||||
[JsonProperty("verified_depots", Required = Required.Always)]
|
||||
internal readonly ImmutableHashSet<uint> VerifiedDepots = ImmutableHashSet<uint>.Empty;
|
||||
[JsonInclude]
|
||||
[JsonPropertyName("verified_depots")]
|
||||
[JsonRequired]
|
||||
internal ImmutableHashSet<uint> VerifiedDepots { get; private init; } = [];
|
||||
|
||||
[JsonProperty("verified_subs", Required = Required.Always)]
|
||||
internal readonly ImmutableHashSet<uint> VerifiedPackages = ImmutableHashSet<uint>.Empty;
|
||||
[JsonInclude]
|
||||
[JsonPropertyName("verified_subs")]
|
||||
[JsonRequired]
|
||||
internal ImmutableHashSet<uint> VerifiedPackages { get; private init; } = [];
|
||||
}
|
||||
#pragma warning restore CA1812 // False positive, the class is used during json deserialization
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
// ----------------------------------------------------------------------------------------------
|
||||
// _ _ _ ____ _ _____
|
||||
// / \ _ __ ___ | |__ (_)/ ___| | |_ ___ __ _ _ __ ___ | ___|__ _ _ __ _ __ ___
|
||||
// / _ \ | '__|/ __|| '_ \ | |\___ \ | __|/ _ \ / _` || '_ ` _ \ | |_ / _` || '__|| '_ ` _ \
|
||||
// / ___ \ | | | (__ | | | || | ___) || |_| __/| (_| || | | | | || _|| (_| || | | | | | | |
|
||||
// /_/ \_\|_| \___||_| |_||_||____/ \__|\___| \__,_||_| |_| |_||_| \__,_||_| |_| |_| |_|
|
||||
// ----------------------------------------------------------------------------------------------
|
||||
// |
|
||||
// Copyright 2015-2023 Łukasz "JustArchi" Domeradzki
|
||||
// Copyright 2015-2024 Łukasz "JustArchi" Domeradzki
|
||||
// Contact: JustArchi@JustArchi.net
|
||||
// |
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
@@ -21,49 +23,57 @@
|
||||
|
||||
using System;
|
||||
using System.Collections.Concurrent;
|
||||
using System.Collections.Frozen;
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.Immutable;
|
||||
using System.Globalization;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Text.Json.Serialization;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using ArchiSteamFarm.Core;
|
||||
using ArchiSteamFarm.Helpers;
|
||||
using ArchiSteamFarm.Helpers.Json;
|
||||
using ArchiSteamFarm.OfficialPlugins.SteamTokenDumper.Localization;
|
||||
using ArchiSteamFarm.Web.Responses;
|
||||
using JetBrains.Annotations;
|
||||
using Newtonsoft.Json;
|
||||
using SteamKit2;
|
||||
|
||||
namespace ArchiSteamFarm.OfficialPlugins.SteamTokenDumper;
|
||||
|
||||
internal sealed class GlobalCache : SerializableFile {
|
||||
internal static readonly ArchiCacheable<ImmutableHashSet<uint>> KnownDepotIDs = new(ResolveKnownDepotIDs, TimeSpan.FromDays(7));
|
||||
internal static readonly ArchiCacheable<FrozenSet<uint>> KnownDepotIDs = new(ResolveKnownDepotIDs, TimeSpan.FromDays(7));
|
||||
|
||||
private static string SharedFilePath => Path.Combine(ArchiSteamFarm.SharedInfo.ConfigDirectory, $"{nameof(SteamTokenDumper)}.cache");
|
||||
|
||||
[JsonProperty(Required = Required.DisallowNull)]
|
||||
private readonly ConcurrentDictionary<uint, uint> AppChangeNumbers = new();
|
||||
|
||||
[JsonProperty(Required = Required.DisallowNull)]
|
||||
private readonly ConcurrentDictionary<uint, ulong> AppTokens = new();
|
||||
|
||||
[JsonProperty(Required = Required.DisallowNull)]
|
||||
private readonly ConcurrentDictionary<uint, string> DepotKeys = new();
|
||||
|
||||
[JsonProperty(Required = Required.DisallowNull)]
|
||||
private readonly ConcurrentDictionary<uint, ulong> SubmittedApps = new();
|
||||
|
||||
[JsonProperty(Required = Required.DisallowNull)]
|
||||
private readonly ConcurrentDictionary<uint, string> SubmittedDepots = new();
|
||||
|
||||
[JsonProperty(Required = Required.DisallowNull)]
|
||||
private readonly ConcurrentDictionary<uint, ulong> SubmittedPackages = new();
|
||||
|
||||
[JsonProperty(Required = Required.DisallowNull)]
|
||||
[JsonInclude]
|
||||
internal uint LastChangeNumber { get; private set; }
|
||||
|
||||
[JsonDisallowNull]
|
||||
[JsonInclude]
|
||||
private ConcurrentDictionary<uint, uint> AppChangeNumbers { get; init; } = new();
|
||||
|
||||
[JsonDisallowNull]
|
||||
[JsonInclude]
|
||||
private ConcurrentDictionary<uint, ulong> AppTokens { get; init; } = new();
|
||||
|
||||
[JsonDisallowNull]
|
||||
[JsonInclude]
|
||||
private ConcurrentDictionary<uint, string> DepotKeys { get; init; } = new();
|
||||
|
||||
[JsonDisallowNull]
|
||||
[JsonInclude]
|
||||
private ConcurrentDictionary<uint, ulong> SubmittedApps { get; init; } = new();
|
||||
|
||||
[JsonDisallowNull]
|
||||
[JsonInclude]
|
||||
private ConcurrentDictionary<uint, string> SubmittedDepots { get; init; } = new();
|
||||
|
||||
[JsonDisallowNull]
|
||||
[JsonInclude]
|
||||
private ConcurrentDictionary<uint, ulong> SubmittedPackages { get; init; } = new();
|
||||
|
||||
[JsonConstructor]
|
||||
internal GlobalCache() => FilePath = SharedFilePath;
|
||||
|
||||
[UsedImplicitly]
|
||||
@@ -87,6 +97,8 @@ internal sealed class GlobalCache : SerializableFile {
|
||||
[UsedImplicitly]
|
||||
public bool ShouldSerializeSubmittedPackages() => !SubmittedPackages.IsEmpty;
|
||||
|
||||
protected override Task Save() => Save(this);
|
||||
|
||||
internal ulong GetAppToken(uint appID) => AppTokens[appID];
|
||||
|
||||
internal Dictionary<uint, ulong> GetAppTokensForSubmission() => AppTokens.Where(appToken => (SteamTokenDumperPlugin.Config?.SecretAppIDs.Contains(appToken.Key) != true) && (appToken.Value > 0) && (!SubmittedApps.TryGetValue(appToken.Key, out ulong token) || (appToken.Value != token))).ToDictionary(static appToken => appToken.Key, static appToken => appToken.Value);
|
||||
@@ -118,7 +130,7 @@ internal sealed class GlobalCache : SerializableFile {
|
||||
return null;
|
||||
}
|
||||
|
||||
globalCache = JsonConvert.DeserializeObject<GlobalCache>(json);
|
||||
globalCache = json.ToJsonObject<GlobalCache>();
|
||||
} catch (Exception e) {
|
||||
ASF.ArchiLogger.LogGenericException(e);
|
||||
|
||||
@@ -306,7 +318,7 @@ internal sealed class GlobalCache : SerializableFile {
|
||||
return (depotKey.Length == 64) && Utilities.IsValidHexadecimalText(depotKey);
|
||||
}
|
||||
|
||||
private static async Task<(bool Success, ImmutableHashSet<uint>? Result)> ResolveKnownDepotIDs(CancellationToken cancellationToken = default) {
|
||||
private static async Task<(bool Success, FrozenSet<uint>? Result)> ResolveKnownDepotIDs(CancellationToken cancellationToken = default) {
|
||||
if (ASF.WebBrowser == null) {
|
||||
throw new InvalidOperationException(nameof(ASF.WebBrowser));
|
||||
}
|
||||
@@ -343,7 +355,7 @@ internal sealed class GlobalCache : SerializableFile {
|
||||
result.Add(depotID);
|
||||
}
|
||||
|
||||
return (result.Count > 0, result.ToImmutableHashSet());
|
||||
return (result.Count > 0, result.ToFrozenSet());
|
||||
} catch (Exception e) {
|
||||
ASF.ArchiLogger.LogGenericWarningException(e);
|
||||
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
// ----------------------------------------------------------------------------------------------
|
||||
// _ _ _ ____ _ _____
|
||||
// / \ _ __ ___ | |__ (_)/ ___| | |_ ___ __ _ _ __ ___ | ___|__ _ _ __ _ __ ___
|
||||
// / _ \ | '__|/ __|| '_ \ | |\___ \ | __|/ _ \ / _` || '_ ` _ \ | |_ / _` || '__|| '_ ` _ \
|
||||
// / ___ \ | | | (__ | | | || | ___) || |_| __/| (_| || | | | | || _|| (_| || | | | | | | |
|
||||
// /_/ \_\|_| \___||_| |_||_||____/ \__|\___| \__,_||_| |_| |_||_| \__,_||_| |_| |_| |_|
|
||||
// ----------------------------------------------------------------------------------------------
|
||||
// |
|
||||
// Copyright 2015-2023 Łukasz "JustArchi" Domeradzki
|
||||
// Copyright 2015-2024 Łukasz "JustArchi" Domeradzki
|
||||
// Contact: JustArchi@JustArchi.net
|
||||
// |
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
@@ -19,16 +21,16 @@
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
using Newtonsoft.Json;
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace ArchiSteamFarm.OfficialPlugins.SteamTokenDumper;
|
||||
|
||||
public sealed class GlobalConfigExtension {
|
||||
[JsonProperty]
|
||||
public SteamTokenDumperConfig? SteamTokenDumperPlugin { get; private set; }
|
||||
[JsonInclude]
|
||||
public SteamTokenDumperConfig? SteamTokenDumperPlugin { get; private init; }
|
||||
|
||||
[JsonProperty(Required = Required.DisallowNull)]
|
||||
public bool SteamTokenDumperPluginEnabled { get; private set; }
|
||||
[JsonInclude]
|
||||
public bool SteamTokenDumperPluginEnabled { get; private init; }
|
||||
|
||||
[JsonConstructor]
|
||||
internal GlobalConfigExtension() { }
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -110,7 +110,7 @@
|
||||
<comment>{0} will be replaced by the number (count this batch) of app infos retrieved</comment>
|
||||
</data>
|
||||
<data name="BotFinishedRetrievingDepotKeys" xml:space="preserve">
|
||||
<value>{0} de {1} chaves do depósito recuperadas com sucesso.</value>
|
||||
<value>{0} de {1} chaves de depots recuperadas com sucesso.</value>
|
||||
<comment>{0} will be replaced by the number (count this batch) of depot keys that were successfully retrieved, {1} will be replaced by the number (count this batch) of depot keys that were supposed to be retrieved</comment>
|
||||
</data>
|
||||
<data name="BotFinishedRetrievingTotalDepots" xml:space="preserve">
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -168,9 +168,9 @@
|
||||
<value>STD genel önbelleği yükleniyor...</value>
|
||||
</data>
|
||||
<data name="ValidatingGlobalCacheIntegrity" xml:space="preserve">
|
||||
<value>STD genel önbellek bütünlüğünü doğrulama...</value>
|
||||
<value>STD genel önbellek bütünlüğünü doğruluyor...</value>
|
||||
</data>
|
||||
<data name="GlobalCacheIntegrityValidationFailed" xml:space="preserve">
|
||||
<value>STD genel önbellek bütünlüğü doğrulanamadı. Bu, olası bir dosya / bellek bozulmasına işaret eder, bunun yerine yeni bir örnek başlatılır.</value>
|
||||
<value>STD genel önbellek bütünlüğü doğrulanamadı. Bu, olası bir dosya/bellek bozulmasına işaret eder, bunun yerine yeni bir örnek başlatılır.</value>
|
||||
</data>
|
||||
</root>
|
||||
|
||||
@@ -97,24 +97,80 @@
|
||||
<value>Закінчено отримання всього {0} токенів доступу.</value>
|
||||
<comment>{0} will be replaced by the number (total count) of app access tokens retrieved</comment>
|
||||
</data>
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
<data name="BotRetrievingTotalDepots" xml:space="preserve">
|
||||
<value>Отримання всіх депо для загалом {0} програм...</value>
|
||||
<comment>{0} will be replaced by the number (total count) of apps being retrieved</comment>
|
||||
</data>
|
||||
<data name="BotRetrievingAppInfos" xml:space="preserve">
|
||||
<value>Отримання {0} інформації про програму...</value>
|
||||
<comment>{0} will be replaced by the number (count this batch) of app infos being retrieved</comment>
|
||||
</data>
|
||||
<data name="BotFinishedRetrievingAppInfos" xml:space="preserve">
|
||||
<value>Завершено отримання {0} інформації про програму.</value>
|
||||
<comment>{0} will be replaced by the number (count this batch) of app infos retrieved</comment>
|
||||
</data>
|
||||
<data name="BotFinishedRetrievingDepotKeys" xml:space="preserve">
|
||||
<value>Успішно отримано {0} з {1} ключів депо.</value>
|
||||
<comment>{0} will be replaced by the number (count this batch) of depot keys that were successfully retrieved, {1} will be replaced by the number (count this batch) of depot keys that were supposed to be retrieved</comment>
|
||||
</data>
|
||||
<data name="BotFinishedRetrievingTotalDepots" xml:space="preserve">
|
||||
<value>Завершено отримання всіх ключів депо для {0} програм.</value>
|
||||
<comment>{0} will be replaced by the number (total count) of apps retrieved</comment>
|
||||
</data>
|
||||
<data name="SubmissionNoNewData" xml:space="preserve">
|
||||
<value>Ніяких нових даних подавати не потрібно, все актуально.</value>
|
||||
</data>
|
||||
<data name="SubmissionNoContributorSet" xml:space="preserve">
|
||||
<value>Не вдалося надіслати дані, оскільки немає дійсного набору SteamID, який ми могли б класифікувати як дописувача. Подумайте про встановлення властивості {0}.</value>
|
||||
<comment>{0} will be replaced by the name of the config property (e.g. "SteamOwnerID") that the user is expected to set</comment>
|
||||
</data>
|
||||
<data name="SubmissionInProgress" xml:space="preserve">
|
||||
<value>Надіслати загальну кількість зареєстрованих програм/пакетів/депо: {0}/{1}/{2}...</value>
|
||||
<comment>{0} will be replaced by the number of app access tokens being submitted, {1} will be replaced by the number of package access tokens being submitted, {2} will be replaced by the number of depot keys being submitted</comment>
|
||||
</data>
|
||||
<data name="SubmissionFailedTooManyRequests" xml:space="preserve">
|
||||
<value>Відправлення не вдалося через занадто велику кількість надісланих запитів, ми спробуємо ще раз приблизно через {0} відтепер.</value>
|
||||
<comment>{0} will be replaced by translated TimeSpan string (such as "53 minutes")</comment>
|
||||
</data>
|
||||
<data name="SubmissionSuccessful" xml:space="preserve">
|
||||
<value>Дані успішно надіслано. На сервері зареєстровано всього нових програм/пакунків/сховищ: {0} ({1} перевірено)/{2} ({3} перевірено)/{4} ({5} перевірено).</value>
|
||||
<comment>{0} will be replaced by the number of new app access tokens that the server has registered, {1} will be replaced by the number of verified app access tokens that the server has registered, {2} will be replaced by the number of new package access tokens that the server has registered, {3} will be replaced by the number of verified package access tokens that the server has registered, {4} will be replaced by the number of new depot keys that the server has registered, {5} will be replaced by the number of verified depot keys that the server has registered</comment>
|
||||
</data>
|
||||
<data name="SubmissionSuccessfulNewApps" xml:space="preserve">
|
||||
<value>Нові програми: {0}</value>
|
||||
<comment>{0} will be replaced by list of the apps (IDs, numbers), separated by a comma</comment>
|
||||
</data>
|
||||
<data name="SubmissionSuccessfulVerifiedApps" xml:space="preserve">
|
||||
<value>Перевірених програм: {0}</value>
|
||||
<comment>{0} will be replaced by list of the apps (IDs, numbers), separated by a comma</comment>
|
||||
</data>
|
||||
<data name="SubmissionSuccessfulNewPackages" xml:space="preserve">
|
||||
<value>Нові пакунки: {0}</value>
|
||||
<comment>{0} will be replaced by list of the packages (IDs, numbers), separated by a comma</comment>
|
||||
</data>
|
||||
<data name="SubmissionSuccessfulVerifiedPackages" xml:space="preserve">
|
||||
<value>Перевірені пакунки: {0}</value>
|
||||
<comment>{0} will be replaced by list of the packages (IDs, numbers), separated by a comma</comment>
|
||||
</data>
|
||||
<data name="SubmissionSuccessfulNewDepots" xml:space="preserve">
|
||||
<value>Нові склади: {0}</value>
|
||||
<comment>{0} will be replaced by list of the depots (IDs, numbers), separated by a comma</comment>
|
||||
</data>
|
||||
<data name="SubmissionSuccessfulVerifiedDepots" xml:space="preserve">
|
||||
<value>Перевірені склади: {0}</value>
|
||||
<comment>{0} will be replaced by list of the depots (IDs, numbers), separated by a comma</comment>
|
||||
</data>
|
||||
<data name="PluginSecretListInitialized" xml:space="preserve">
|
||||
<value>{0} ініціалізовано, плагін не буде вирішувати жодного з них: {1}.</value>
|
||||
<comment>{0} will be replaced by the name of the config property (e.g. "SecretPackageIDs"), {1} will be replaced by list of the objects (IDs, numbers), separated by a comma</comment>
|
||||
</data>
|
||||
<data name="LoadingGlobalCache" xml:space="preserve">
|
||||
<value>Завантаження глобального кешу STD...</value>
|
||||
</data>
|
||||
<data name="ValidatingGlobalCacheIntegrity" xml:space="preserve">
|
||||
<value>Перевірка цілісності глобального кешу STD...</value>
|
||||
</data>
|
||||
<data name="GlobalCacheIntegrityValidationFailed" xml:space="preserve">
|
||||
<value>Не вдалося перевірити цілісність глобального кешу STD. Це свідчить про можливе пошкодження файлу/пам'яті, замість нього буде ініціалізовано новий екземпляр.</value>
|
||||
</data>
|
||||
</root>
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
// ----------------------------------------------------------------------------------------------
|
||||
// _ _ _ ____ _ _____
|
||||
// / \ _ __ ___ | |__ (_)/ ___| | |_ ___ __ _ _ __ ___ | ___|__ _ _ __ _ __ ___
|
||||
// / _ \ | '__|/ __|| '_ \ | |\___ \ | __|/ _ \ / _` || '_ ` _ \ | |_ / _` || '__|| '_ ` _ \
|
||||
// / ___ \ | | | (__ | | | || | ___) || |_| __/| (_| || | | | | || _|| (_| || | | | | | | |
|
||||
// /_/ \_\|_| \___||_| |_||_||____/ \__|\___| \__,_||_| |_| |_||_| \__,_||_| |_| |_| |_|
|
||||
// ----------------------------------------------------------------------------------------------
|
||||
// |
|
||||
// Copyright 2015-2023 Łukasz "JustArchi" Domeradzki
|
||||
// Copyright 2015-2024 Łukasz "JustArchi" Domeradzki
|
||||
// Contact: JustArchi@JustArchi.net
|
||||
// |
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
// ----------------------------------------------------------------------------------------------
|
||||
// _ _ _ ____ _ _____
|
||||
// / \ _ __ ___ | |__ (_)/ ___| | |_ ___ __ _ _ __ ___ | ___|__ _ _ __ _ __ ___
|
||||
// / _ \ | '__|/ __|| '_ \ | |\___ \ | __|/ _ \ / _` || '_ ` _ \ | |_ / _` || '__|| '_ ` _ \
|
||||
// / ___ \ | | | (__ | | | || | ___) || |_| __/| (_| || | | | | || _|| (_| || | | | | | | |
|
||||
// /_/ \_\|_| \___||_| |_||_||____/ \__|\___| \__,_||_| |_| |_||_| \__,_||_| |_| |_| |_|
|
||||
// ----------------------------------------------------------------------------------------------
|
||||
// |
|
||||
// Copyright 2015-2023 Łukasz "JustArchi" Domeradzki
|
||||
// Copyright 2015-2024 Łukasz "JustArchi" Domeradzki
|
||||
// Contact: JustArchi@JustArchi.net
|
||||
// |
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
@@ -20,29 +22,33 @@
|
||||
// limitations under the License.
|
||||
|
||||
using System.Collections.Immutable;
|
||||
using System.Text.Json.Serialization;
|
||||
using ArchiSteamFarm.Helpers.Json;
|
||||
using ArchiSteamFarm.IPC.Integration;
|
||||
using Newtonsoft.Json;
|
||||
|
||||
namespace ArchiSteamFarm.OfficialPlugins.SteamTokenDumper;
|
||||
|
||||
public sealed class SteamTokenDumperConfig {
|
||||
[JsonProperty(Required = Required.DisallowNull)]
|
||||
[JsonInclude]
|
||||
public bool Enabled { get; internal set; }
|
||||
|
||||
[JsonProperty(Required = Required.DisallowNull)]
|
||||
[JsonDisallowNull]
|
||||
[JsonInclude]
|
||||
[SwaggerItemsMinMax(MinimumUint = 1, MaximumUint = uint.MaxValue)]
|
||||
public ImmutableHashSet<uint> SecretAppIDs { get; private set; } = ImmutableHashSet<uint>.Empty;
|
||||
public ImmutableHashSet<uint> SecretAppIDs { get; private init; } = [];
|
||||
|
||||
[JsonProperty(Required = Required.DisallowNull)]
|
||||
[JsonDisallowNull]
|
||||
[JsonInclude]
|
||||
[SwaggerItemsMinMax(MinimumUint = 1, MaximumUint = uint.MaxValue)]
|
||||
public ImmutableHashSet<uint> SecretDepotIDs { get; private set; } = ImmutableHashSet<uint>.Empty;
|
||||
public ImmutableHashSet<uint> SecretDepotIDs { get; private init; } = [];
|
||||
|
||||
[JsonProperty(Required = Required.DisallowNull)]
|
||||
[JsonDisallowNull]
|
||||
[JsonInclude]
|
||||
[SwaggerItemsMinMax(MinimumUint = 1, MaximumUint = uint.MaxValue)]
|
||||
public ImmutableHashSet<uint> SecretPackageIDs { get; private set; } = ImmutableHashSet<uint>.Empty;
|
||||
public ImmutableHashSet<uint> SecretPackageIDs { get; private init; } = [];
|
||||
|
||||
[JsonProperty(Required = Required.DisallowNull)]
|
||||
public bool SkipAutoGrantPackages { get; private set; } = true;
|
||||
[JsonInclude]
|
||||
public bool SkipAutoGrantPackages { get; private init; } = true;
|
||||
|
||||
[JsonConstructor]
|
||||
internal SteamTokenDumperConfig() { }
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
// ----------------------------------------------------------------------------------------------
|
||||
// _ _ _ ____ _ _____
|
||||
// / \ _ __ ___ | |__ (_)/ ___| | |_ ___ __ _ _ __ ___ | ___|__ _ _ __ _ __ ___
|
||||
// / _ \ | '__|/ __|| '_ \ | |\___ \ | __|/ _ \ / _` || '_ ` _ \ | |_ / _` || '__|| '_ ` _ \
|
||||
// / ___ \ | | | (__ | | | || | ___) || |_| __/| (_| || | | | | || _|| (_| || | | | | | | |
|
||||
// /_/ \_\|_| \___||_| |_||_||____/ \__|\___| \__,_||_| |_| |_||_| \__,_||_| |_| |_| |_|
|
||||
// ----------------------------------------------------------------------------------------------
|
||||
// |
|
||||
// Copyright 2015-2023 Łukasz "JustArchi" Domeradzki
|
||||
// Copyright 2015-2024 Łukasz "JustArchi" Domeradzki
|
||||
// Contact: JustArchi@JustArchi.net
|
||||
// |
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
@@ -30,6 +32,6 @@ namespace ArchiSteamFarm.OfficialPlugins.SteamTokenDumper;
|
||||
public sealed class SteamTokenDumperController : ArchiController {
|
||||
[HttpGet(nameof(GlobalConfigExtension))]
|
||||
[ProducesResponseType<GlobalConfigExtension>((int) HttpStatusCode.OK)]
|
||||
[SwaggerOperation(Tags = new[] { nameof(GlobalConfigExtension) })]
|
||||
[SwaggerOperation(Tags = [nameof(GlobalConfigExtension)])]
|
||||
public ActionResult<GlobalConfigExtension> Get() => Ok(new GlobalConfigExtension());
|
||||
}
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
// ----------------------------------------------------------------------------------------------
|
||||
// _ _ _ ____ _ _____
|
||||
// / \ _ __ ___ | |__ (_)/ ___| | |_ ___ __ _ _ __ ___ | ___|__ _ _ __ _ __ ___
|
||||
// / _ \ | '__|/ __|| '_ \ | |\___ \ | __|/ _ \ / _` || '_ ` _ \ | |_ / _` || '__|| '_ ` _ \
|
||||
// / ___ \ | | | (__ | | | || | ___) || |_| __/| (_| || | | | | || _|| (_| || | | | | | | |
|
||||
// /_/ \_\|_| \___||_| |_||_||____/ \__|\___| \__,_||_| |_| |_||_| \__,_||_| |_| |_| |_|
|
||||
// ----------------------------------------------------------------------------------------------
|
||||
// |
|
||||
// Copyright 2015-2023 Łukasz "JustArchi" Domeradzki
|
||||
// Copyright 2015-2024 Łukasz "JustArchi" Domeradzki
|
||||
// Contact: JustArchi@JustArchi.net
|
||||
// |
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
@@ -21,17 +23,21 @@
|
||||
|
||||
using System;
|
||||
using System.Collections.Concurrent;
|
||||
using System.Collections.Frozen;
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.Immutable;
|
||||
using System.ComponentModel;
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using System.Composition;
|
||||
using System.Globalization;
|
||||
using System.Linq;
|
||||
using System.Net;
|
||||
using System.Text.Json;
|
||||
using System.Text.Json.Serialization;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using ArchiSteamFarm.Core;
|
||||
using ArchiSteamFarm.Helpers;
|
||||
using ArchiSteamFarm.Helpers.Json;
|
||||
using ArchiSteamFarm.OfficialPlugins.SteamTokenDumper.Data;
|
||||
using ArchiSteamFarm.OfficialPlugins.SteamTokenDumper.Localization;
|
||||
using ArchiSteamFarm.Plugins;
|
||||
@@ -41,8 +47,6 @@ using ArchiSteamFarm.Steam.Interaction;
|
||||
using ArchiSteamFarm.Storage;
|
||||
using ArchiSteamFarm.Web;
|
||||
using ArchiSteamFarm.Web.Responses;
|
||||
using Newtonsoft.Json;
|
||||
using Newtonsoft.Json.Linq;
|
||||
using SteamKit2;
|
||||
|
||||
namespace ArchiSteamFarm.OfficialPlugins.SteamTokenDumper;
|
||||
@@ -51,7 +55,6 @@ namespace ArchiSteamFarm.OfficialPlugins.SteamTokenDumper;
|
||||
internal sealed class SteamTokenDumperPlugin : OfficialPlugin, IASF, IBot, IBotCommand2, IBotSteamClient, ISteamPICSChanges {
|
||||
private const ushort DepotsRateLimitingDelay = 500;
|
||||
|
||||
[JsonProperty]
|
||||
internal static SteamTokenDumperConfig? Config { get; private set; }
|
||||
|
||||
private static readonly ConcurrentDictionary<Bot, IDisposable> BotSubscriptions = new();
|
||||
@@ -62,15 +65,17 @@ internal sealed class SteamTokenDumperPlugin : OfficialPlugin, IASF, IBot, IBotC
|
||||
private static GlobalCache? GlobalCache;
|
||||
private static DateTimeOffset LastUploadAt = DateTimeOffset.MinValue;
|
||||
|
||||
[JsonProperty]
|
||||
[JsonInclude]
|
||||
[Required]
|
||||
public override string Name => nameof(SteamTokenDumperPlugin);
|
||||
|
||||
[JsonProperty]
|
||||
[JsonInclude]
|
||||
[Required]
|
||||
public override Version Version => typeof(SteamTokenDumperPlugin).Assembly.GetName().Version ?? throw new InvalidOperationException(nameof(Version));
|
||||
|
||||
public Task<uint> GetPreferredChangeNumberToStartFrom() => Task.FromResult(GlobalCache?.LastChangeNumber ?? 0);
|
||||
|
||||
public async Task OnASFInit(IReadOnlyDictionary<string, JToken>? additionalConfigProperties = null) {
|
||||
public async Task OnASFInit(IReadOnlyDictionary<string, JsonElement>? additionalConfigProperties = null) {
|
||||
if (!SharedInfo.HasValidToken) {
|
||||
ASF.ArchiLogger.LogGenericError(string.Format(CultureInfo.CurrentCulture, Strings.PluginDisabledMissingBuildToken, nameof(SteamTokenDumperPlugin)));
|
||||
|
||||
@@ -81,15 +86,19 @@ internal sealed class SteamTokenDumperPlugin : OfficialPlugin, IASF, IBot, IBotC
|
||||
SteamTokenDumperConfig? config = null;
|
||||
|
||||
if (additionalConfigProperties != null) {
|
||||
foreach ((string configProperty, JToken configValue) in additionalConfigProperties) {
|
||||
foreach ((string configProperty, JsonElement configValue) in additionalConfigProperties) {
|
||||
try {
|
||||
switch (configProperty) {
|
||||
case nameof(GlobalConfigExtension.SteamTokenDumperPlugin):
|
||||
config = configValue.ToObject<SteamTokenDumperConfig>();
|
||||
config = configValue.ToJsonObject<SteamTokenDumperConfig>();
|
||||
|
||||
break;
|
||||
case nameof(GlobalConfigExtension.SteamTokenDumperPluginEnabled):
|
||||
isEnabled = configValue.Value<bool>();
|
||||
case nameof(GlobalConfigExtension.SteamTokenDumperPluginEnabled) when configValue.ValueKind == JsonValueKind.False:
|
||||
isEnabled = false;
|
||||
|
||||
break;
|
||||
case nameof(GlobalConfigExtension.SteamTokenDumperPluginEnabled) when configValue.ValueKind == JsonValueKind.True:
|
||||
isEnabled = true;
|
||||
|
||||
break;
|
||||
}
|
||||
@@ -160,7 +169,7 @@ internal sealed class SteamTokenDumperPlugin : OfficialPlugin, IASF, IBot, IBotC
|
||||
ASF.ArchiLogger.LogGenericInfo(string.Format(CultureInfo.CurrentCulture, Strings.PluginInitializedAndEnabled, nameof(SteamTokenDumperPlugin), startIn.ToHumanReadable()));
|
||||
}
|
||||
|
||||
public async Task<string?> OnBotCommand(Bot bot, EAccess access, string message, string[] args, ulong steamID = 0) {
|
||||
public Task<string?> OnBotCommand(Bot bot, EAccess access, string message, string[] args, ulong steamID = 0) {
|
||||
ArgumentNullException.ThrowIfNull(bot);
|
||||
|
||||
if (!Enum.IsDefined(access)) {
|
||||
@@ -175,20 +184,20 @@ internal sealed class SteamTokenDumperPlugin : OfficialPlugin, IASF, IBot, IBotC
|
||||
case 1:
|
||||
switch (args[0].ToUpperInvariant()) {
|
||||
case "STD":
|
||||
return await ResponseRefreshManually(access, bot).ConfigureAwait(false);
|
||||
return Task.FromResult(ResponseRefreshManually(access, bot));
|
||||
}
|
||||
|
||||
break;
|
||||
default:
|
||||
switch (args[0].ToUpperInvariant()) {
|
||||
case "STD":
|
||||
return await ResponseRefreshManually(access, Utilities.GetArgsAsText(args, 1, ","), steamID).ConfigureAwait(false);
|
||||
return Task.FromResult(ResponseRefreshManually(access, Utilities.GetArgsAsText(args, 1, ","), steamID));
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
return null;
|
||||
return Task.FromResult((string?) null);
|
||||
}
|
||||
|
||||
public async Task OnBotDestroy(Bot bot) {
|
||||
@@ -289,14 +298,26 @@ internal sealed class SteamTokenDumperPlugin : OfficialPlugin, IASF, IBot, IBotC
|
||||
return;
|
||||
}
|
||||
|
||||
HashSet<uint> packageIDs = callback.LicenseList.Where(static license => !Config.SecretPackageIDs.Contains(license.PackageID) && ((license.PaymentMethod != EPaymentMethod.AutoGrant) || !Config.SkipAutoGrantPackages)).Select(static license => license.PackageID).ToHashSet();
|
||||
// Schedule a refresh in a while from now
|
||||
if (!BotSynchronizations.TryGetValue(bot, out (SemaphoreSlim RefreshSemaphore, Timer RefreshTimer) synchronization)) {
|
||||
return;
|
||||
}
|
||||
|
||||
await Refresh(bot, packageIDs).ConfigureAwait(false);
|
||||
if (!await synchronization.RefreshSemaphore.WaitAsync(0).ConfigureAwait(false)) {
|
||||
// Another refresh is in progress, skip the refresh for now
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
synchronization.RefreshTimer.Change(TimeSpan.FromMinutes(1), TimeSpan.FromHours(SharedInfo.MaximumHoursBetweenRefresh));
|
||||
} finally {
|
||||
synchronization.RefreshSemaphore.Release();
|
||||
}
|
||||
}
|
||||
|
||||
private static async void OnSubmissionTimer(object? state = null) => await SubmitData().ConfigureAwait(false);
|
||||
|
||||
private static async Task Refresh(Bot bot, IReadOnlyCollection<uint>? packageIDs = null) {
|
||||
private static async Task Refresh(Bot bot) {
|
||||
ArgumentNullException.ThrowIfNull(bot);
|
||||
|
||||
if (GlobalCache == null) {
|
||||
@@ -322,9 +343,9 @@ internal sealed class SteamTokenDumperPlugin : OfficialPlugin, IASF, IBot, IBotC
|
||||
return;
|
||||
}
|
||||
|
||||
packageIDs ??= bot.OwnedPackageIDs.Where(static package => (Config?.SecretPackageIDs.Contains(package.Key) != true) && ((package.Value.PaymentMethod != EPaymentMethod.AutoGrant) || (Config?.SkipAutoGrantPackages == false))).Select(static package => package.Key).ToHashSet();
|
||||
HashSet<uint> packageIDs = bot.OwnedPackageIDs.Where(static package => (Config?.SecretPackageIDs.Contains(package.Key) != true) && ((package.Value.PaymentMethod != EPaymentMethod.AutoGrant) || (Config?.SkipAutoGrantPackages == false))).Select(static package => package.Key).ToHashSet();
|
||||
|
||||
HashSet<uint> appIDsToRefresh = new();
|
||||
HashSet<uint> appIDsToRefresh = [];
|
||||
|
||||
foreach (uint packageID in packageIDs.Where(static packageID => Config?.SecretPackageIDs.Contains(packageID) != true)) {
|
||||
if (!ASF.GlobalDatabase.PackagesDataReadOnly.TryGetValue(packageID, out PackageData? packageData) || (packageData.AppIDs == null)) {
|
||||
@@ -384,7 +405,7 @@ internal sealed class SteamTokenDumperPlugin : OfficialPlugin, IASF, IBot, IBotC
|
||||
bot.ArchiLogger.LogGenericInfo(string.Format(CultureInfo.CurrentCulture, Strings.BotFinishedRetrievingTotalAppAccessTokens, appIDsToRefresh.Count));
|
||||
bot.ArchiLogger.LogGenericInfo(string.Format(CultureInfo.CurrentCulture, Strings.BotRetrievingTotalDepots, appIDsToRefresh.Count));
|
||||
|
||||
(_, ImmutableHashSet<uint>? knownDepotIDs) = await GlobalCache.KnownDepotIDs.GetValue(ECacheFallback.SuccessPreviously).ConfigureAwait(false);
|
||||
(_, FrozenSet<uint>? knownDepotIDs) = await GlobalCache.KnownDepotIDs.GetValue(ECacheFallback.SuccessPreviously).ConfigureAwait(false);
|
||||
|
||||
using (HashSet<uint>.Enumerator enumerator = appIDsToRefresh.GetEnumerator()) {
|
||||
while (true) {
|
||||
@@ -500,7 +521,9 @@ internal sealed class SteamTokenDumperPlugin : OfficialPlugin, IASF, IBot, IBotC
|
||||
}
|
||||
}
|
||||
|
||||
bot.ArchiLogger.LogGenericInfo(string.Format(CultureInfo.CurrentCulture, Strings.BotFinishedRetrievingDepotKeys, depotKeysSuccessful, depotKeysTotal));
|
||||
if (depotKeysTotal > 0) {
|
||||
bot.ArchiLogger.LogGenericInfo(string.Format(CultureInfo.CurrentCulture, Strings.BotFinishedRetrievingDepotKeys, depotKeysSuccessful, depotKeysTotal));
|
||||
}
|
||||
|
||||
if (depotKeysSuccessful < depotKeysTotal) {
|
||||
// We're not going to record app change numbers, as we didn't fetch all the depot keys we wanted
|
||||
@@ -527,7 +550,7 @@ internal sealed class SteamTokenDumperPlugin : OfficialPlugin, IASF, IBot, IBotC
|
||||
}
|
||||
}
|
||||
|
||||
private static async Task<string?> ResponseRefreshManually(EAccess access, Bot bot, bool submit = true) {
|
||||
private static string? ResponseRefreshManually(EAccess access, Bot bot) {
|
||||
if (!Enum.IsDefined(access)) {
|
||||
throw new InvalidEnumArgumentException(nameof(access), (int) access, typeof(EAccess));
|
||||
}
|
||||
@@ -542,16 +565,17 @@ internal sealed class SteamTokenDumperPlugin : OfficialPlugin, IASF, IBot, IBotC
|
||||
return bot.Commands.FormatBotResponse(string.Format(CultureInfo.CurrentCulture, ArchiSteamFarm.Localization.Strings.WarningFailedWithError, nameof(GlobalCache)));
|
||||
}
|
||||
|
||||
await Refresh(bot).ConfigureAwait(false);
|
||||
|
||||
if (submit) {
|
||||
Utilities.InBackground(static () => SubmitData());
|
||||
}
|
||||
Utilities.InBackground(
|
||||
async () => {
|
||||
await Refresh(bot).ConfigureAwait(false);
|
||||
await SubmitData().ConfigureAwait(false);
|
||||
}
|
||||
);
|
||||
|
||||
return bot.Commands.FormatBotResponse(ArchiSteamFarm.Localization.Strings.Done);
|
||||
}
|
||||
|
||||
private static async Task<string?> ResponseRefreshManually(EAccess access, string botNames, ulong steamID = 0) {
|
||||
private static string? ResponseRefreshManually(EAccess access, string botNames, ulong steamID = 0) {
|
||||
if (!Enum.IsDefined(access)) {
|
||||
throw new InvalidEnumArgumentException(nameof(access), (int) access, typeof(EAccess));
|
||||
}
|
||||
@@ -568,17 +592,25 @@ internal sealed class SteamTokenDumperPlugin : OfficialPlugin, IASF, IBot, IBotC
|
||||
return access >= EAccess.Owner ? Commands.FormatStaticResponse(string.Format(CultureInfo.CurrentCulture, ArchiSteamFarm.Localization.Strings.BotNotFound, botNames)) : null;
|
||||
}
|
||||
|
||||
if (bots.RemoveWhere(bot => Commands.GetProxyAccess(bot, access, steamID) < EAccess.Master) > 0) {
|
||||
if (bots.Count == 0) {
|
||||
return access >= EAccess.Owner ? Commands.FormatStaticResponse(string.Format(CultureInfo.CurrentCulture, ArchiSteamFarm.Localization.Strings.BotNotFound, botNames)) : null;
|
||||
}
|
||||
}
|
||||
|
||||
if (GlobalCache == null) {
|
||||
return Commands.FormatStaticResponse(string.Format(CultureInfo.CurrentCulture, ArchiSteamFarm.Localization.Strings.WarningFailedWithError, nameof(GlobalCache)));
|
||||
}
|
||||
|
||||
IList<string?> results = await Utilities.InParallel(bots.Select(bot => ResponseRefreshManually(Commands.GetProxyAccess(bot, access, steamID), bot, false))).ConfigureAwait(false);
|
||||
Utilities.InBackground(
|
||||
async () => {
|
||||
await Utilities.InParallel(bots.Select(static bot => Refresh(bot))).ConfigureAwait(false);
|
||||
|
||||
Utilities.InBackground(static () => SubmitData());
|
||||
await SubmitData().ConfigureAwait(false);
|
||||
}
|
||||
);
|
||||
|
||||
List<string> responses = new(results.Where(static result => !string.IsNullOrEmpty(result))!);
|
||||
|
||||
return responses.Count > 0 ? string.Join(Environment.NewLine, responses) : null;
|
||||
return Commands.FormatStaticResponse(ArchiSteamFarm.Localization.Strings.Done);
|
||||
}
|
||||
|
||||
private static async Task SubmitData(CancellationToken cancellationToken = default) {
|
||||
@@ -680,7 +712,7 @@ internal sealed class SteamTokenDumperPlugin : OfficialPlugin, IASF, IBot, IBotC
|
||||
}
|
||||
|
||||
if (response.Content.Data == null) {
|
||||
ASF.ArchiLogger.LogGenericError(string.Format(CultureInfo.CurrentCulture, ArchiSteamFarm.Localization.Strings.ErrorIsInvalid), nameof(response.Content.Data));
|
||||
ASF.ArchiLogger.LogGenericError(string.Format(CultureInfo.CurrentCulture, ArchiSteamFarm.Localization.Strings.ErrorIsInvalid, nameof(response.Content.Data)));
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
// ----------------------------------------------------------------------------------------------
|
||||
// _ _ _ ____ _ _____
|
||||
// / \ _ __ ___ | |__ (_)/ ___| | |_ ___ __ _ _ __ ___ | ___|__ _ _ __ _ __ ___
|
||||
// / _ \ | '__|/ __|| '_ \ | |\___ \ | __|/ _ \ / _` || '_ ` _ \ | |_ / _` || '__|| '_ ` _ \
|
||||
// / ___ \ | | | (__ | | | || | ___) || |_| __/| (_| || | | | | || _|| (_| || | | | | | | |
|
||||
// /_/ \_\|_| \___||_| |_||_||____/ \__|\___| \__,_||_| |_| |_||_| \__,_||_| |_| |_| |_|
|
||||
// ----------------------------------------------------------------------------------------------
|
||||
// |
|
||||
// Copyright 2015-2023 Łukasz "JustArchi" Domeradzki
|
||||
// Copyright 2015-2024 Łukasz "JustArchi" Domeradzki
|
||||
// Contact: JustArchi@JustArchi.net
|
||||
// |
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
// ----------------------------------------------------------------------------------------------
|
||||
// _ _ _ ____ _ _____
|
||||
// / \ _ __ ___ | |__ (_)/ ___| | |_ ___ __ _ _ __ ___ | ___|__ _ _ __ _ __ ___
|
||||
// / _ \ | '__|/ __|| '_ \ | |\___ \ | __|/ _ \ / _` || '_ ` _ \ | |_ / _` || '__|| '_ ` _ \
|
||||
// / ___ \ | | | (__ | | | || | ___) || |_| __/| (_| || | | | | || _|| (_| || | | | | | | |
|
||||
// /_/ \_\|_| \___||_| |_||_||____/ \__|\___| \__,_||_| |_| |_||_| \__,_||_| |_| |_| |_|
|
||||
// ----------------------------------------------------------------------------------------------
|
||||
// |
|
||||
// Copyright 2015-2023 Łukasz "JustArchi" Domeradzki
|
||||
// Copyright 2015-2024 Łukasz "JustArchi" Domeradzki
|
||||
// Contact: JustArchi@JustArchi.net
|
||||
// |
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
@@ -39,11 +41,11 @@ public sealed class Bot {
|
||||
{ 43, MinCardsPerBadge + 1 }
|
||||
};
|
||||
|
||||
HashSet<Asset> items = new();
|
||||
HashSet<Asset> items = [];
|
||||
|
||||
foreach ((uint appID, byte cards) in itemsPerSet) {
|
||||
for (byte i = 1; i <= cards; i++) {
|
||||
items.Add(CreateCard(i, appID));
|
||||
items.Add(CreateCard(i, realAppID: appID));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -55,30 +57,27 @@ public sealed class Bot {
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
[ExpectedException(typeof(ArgumentOutOfRangeException))]
|
||||
public void MaxItemsTooSmall() {
|
||||
const uint appID = 42;
|
||||
|
||||
HashSet<Asset> items = new() {
|
||||
CreateCard(1, appID),
|
||||
CreateCard(2, appID)
|
||||
};
|
||||
HashSet<Asset> items = [
|
||||
CreateCard(1, realAppID: appID),
|
||||
CreateCard(2, realAppID: appID)
|
||||
];
|
||||
|
||||
GetItemsForFullBadge(items, 2, appID, MinCardsPerBadge - 1);
|
||||
|
||||
Assert.Fail();
|
||||
Assert.ThrowsException<ArgumentOutOfRangeException>(() => GetItemsForFullBadge(items, 2, appID, MinCardsPerBadge - 1));
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void MoreCardsThanNeeded() {
|
||||
const uint appID = 42;
|
||||
|
||||
HashSet<Asset> items = new() {
|
||||
CreateCard(1, appID),
|
||||
CreateCard(1, appID),
|
||||
CreateCard(2, appID),
|
||||
CreateCard(3, appID)
|
||||
};
|
||||
HashSet<Asset> items = [
|
||||
CreateCard(1, realAppID: appID),
|
||||
CreateCard(1, realAppID: appID),
|
||||
CreateCard(2, realAppID: appID),
|
||||
CreateCard(3, realAppID: appID)
|
||||
];
|
||||
|
||||
HashSet<Asset> itemsToSend = GetItemsForFullBadge(items, 3, appID);
|
||||
|
||||
@@ -95,12 +94,12 @@ public sealed class Bot {
|
||||
public void MultipleSets() {
|
||||
const uint appID = 42;
|
||||
|
||||
HashSet<Asset> items = new() {
|
||||
CreateCard(1, appID),
|
||||
CreateCard(1, appID),
|
||||
CreateCard(2, appID),
|
||||
CreateCard(2, appID)
|
||||
};
|
||||
HashSet<Asset> items = [
|
||||
CreateCard(1, realAppID: appID),
|
||||
CreateCard(1, realAppID: appID),
|
||||
CreateCard(2, realAppID: appID),
|
||||
CreateCard(2, realAppID: appID)
|
||||
];
|
||||
|
||||
HashSet<Asset> itemsToSend = GetItemsForFullBadge(items, 2, appID);
|
||||
|
||||
@@ -116,11 +115,11 @@ public sealed class Bot {
|
||||
public void MultipleSetsDifferentAmount() {
|
||||
const uint appID = 42;
|
||||
|
||||
HashSet<Asset> items = new() {
|
||||
CreateCard(1, appID, 2),
|
||||
CreateCard(2, appID),
|
||||
CreateCard(2, appID)
|
||||
};
|
||||
HashSet<Asset> items = [
|
||||
CreateCard(1, amount: 2, realAppID: appID),
|
||||
CreateCard(2, realAppID: appID),
|
||||
CreateCard(2, realAppID: appID)
|
||||
];
|
||||
|
||||
HashSet<Asset> itemsToSend = GetItemsForFullBadge(items, 2, appID);
|
||||
|
||||
@@ -136,29 +135,29 @@ public sealed class Bot {
|
||||
public void MutliRarityAndType() {
|
||||
const uint appID = 42;
|
||||
|
||||
HashSet<Asset> items = new() {
|
||||
CreateCard(1, appID, type: Asset.EType.TradingCard, rarity: Asset.ERarity.Common),
|
||||
CreateCard(2, appID, type: Asset.EType.TradingCard, rarity: Asset.ERarity.Common),
|
||||
HashSet<Asset> items = [
|
||||
CreateCard(1, realAppID: appID, type: EAssetType.TradingCard, rarity: EAssetRarity.Common),
|
||||
CreateCard(2, realAppID: appID, type: EAssetType.TradingCard, rarity: EAssetRarity.Common),
|
||||
|
||||
CreateCard(1, appID, type: Asset.EType.FoilTradingCard, rarity: Asset.ERarity.Uncommon),
|
||||
CreateCard(2, appID, type: Asset.EType.FoilTradingCard, rarity: Asset.ERarity.Uncommon),
|
||||
CreateCard(1, realAppID: appID, type: EAssetType.FoilTradingCard, rarity: EAssetRarity.Uncommon),
|
||||
CreateCard(2, realAppID: appID, type: EAssetType.FoilTradingCard, rarity: EAssetRarity.Uncommon),
|
||||
|
||||
CreateCard(1, appID, type: Asset.EType.FoilTradingCard, rarity: Asset.ERarity.Rare),
|
||||
CreateCard(2, appID, type: Asset.EType.FoilTradingCard, rarity: Asset.ERarity.Rare),
|
||||
CreateCard(1, realAppID: appID, type: EAssetType.FoilTradingCard, rarity: EAssetRarity.Rare),
|
||||
CreateCard(2, realAppID: appID, type: EAssetType.FoilTradingCard, rarity: EAssetRarity.Rare),
|
||||
|
||||
// for better readability and easier verification when thinking about this test the items that shall be selected for sending are the ones below this comment
|
||||
CreateCard(1, appID, type: Asset.EType.TradingCard, rarity: Asset.ERarity.Uncommon),
|
||||
CreateCard(2, appID, type: Asset.EType.TradingCard, rarity: Asset.ERarity.Uncommon),
|
||||
CreateCard(3, appID, type: Asset.EType.TradingCard, rarity: Asset.ERarity.Uncommon),
|
||||
CreateCard(1, realAppID: appID, type: EAssetType.TradingCard, rarity: EAssetRarity.Uncommon),
|
||||
CreateCard(2, realAppID: appID, type: EAssetType.TradingCard, rarity: EAssetRarity.Uncommon),
|
||||
CreateCard(3, realAppID: appID, type: EAssetType.TradingCard, rarity: EAssetRarity.Uncommon),
|
||||
|
||||
CreateCard(1, appID, type: Asset.EType.FoilTradingCard, rarity: Asset.ERarity.Common),
|
||||
CreateCard(3, appID, type: Asset.EType.FoilTradingCard, rarity: Asset.ERarity.Common),
|
||||
CreateCard(7, appID, type: Asset.EType.FoilTradingCard, rarity: Asset.ERarity.Common),
|
||||
CreateCard(1, realAppID: appID, type: EAssetType.FoilTradingCard, rarity: EAssetRarity.Common),
|
||||
CreateCard(3, realAppID: appID, type: EAssetType.FoilTradingCard, rarity: EAssetRarity.Common),
|
||||
CreateCard(7, realAppID: appID, type: EAssetType.FoilTradingCard, rarity: EAssetRarity.Common),
|
||||
|
||||
CreateCard(2, appID, type: Asset.EType.Unknown, rarity: Asset.ERarity.Rare),
|
||||
CreateCard(3, appID, type: Asset.EType.Unknown, rarity: Asset.ERarity.Rare),
|
||||
CreateCard(4, appID, type: Asset.EType.Unknown, rarity: Asset.ERarity.Rare)
|
||||
};
|
||||
CreateCard(2, realAppID: appID, type: EAssetType.Unknown, rarity: EAssetRarity.Rare),
|
||||
CreateCard(3, realAppID: appID, type: EAssetType.Unknown, rarity: EAssetRarity.Rare),
|
||||
CreateCard(4, realAppID: appID, type: EAssetType.Unknown, rarity: EAssetRarity.Rare)
|
||||
];
|
||||
|
||||
HashSet<Asset> itemsToSend = GetItemsForFullBadge(items, 3, appID);
|
||||
|
||||
@@ -177,10 +176,10 @@ public sealed class Bot {
|
||||
public void NotAllCardsPresent() {
|
||||
const uint appID = 42;
|
||||
|
||||
HashSet<Asset> items = new() {
|
||||
CreateCard(1, appID),
|
||||
CreateCard(2, appID)
|
||||
};
|
||||
HashSet<Asset> items = [
|
||||
CreateCard(1, realAppID: appID),
|
||||
CreateCard(2, realAppID: appID)
|
||||
];
|
||||
|
||||
HashSet<Asset> itemsToSend = GetItemsForFullBadge(items, 3, appID);
|
||||
|
||||
@@ -192,10 +191,10 @@ public sealed class Bot {
|
||||
public void OneSet() {
|
||||
const uint appID = 42;
|
||||
|
||||
HashSet<Asset> items = new() {
|
||||
CreateCard(1, appID),
|
||||
CreateCard(2, appID)
|
||||
};
|
||||
HashSet<Asset> items = [
|
||||
CreateCard(1, realAppID: appID),
|
||||
CreateCard(2, realAppID: appID)
|
||||
];
|
||||
|
||||
HashSet<Asset> itemsToSend = GetItemsForFullBadge(items, 2, appID);
|
||||
|
||||
@@ -212,10 +211,10 @@ public sealed class Bot {
|
||||
const uint appID0 = 42;
|
||||
const uint appID1 = 43;
|
||||
|
||||
HashSet<Asset> items = new() {
|
||||
CreateCard(1, appID0),
|
||||
CreateCard(1, appID1)
|
||||
};
|
||||
HashSet<Asset> items = [
|
||||
CreateCard(1, realAppID: appID0),
|
||||
CreateCard(1, realAppID: appID1)
|
||||
];
|
||||
|
||||
HashSet<Asset> itemsToSend = GetItemsForFullBadge(
|
||||
items, new Dictionary<uint, byte> {
|
||||
@@ -237,10 +236,10 @@ public sealed class Bot {
|
||||
const uint appID0 = 42;
|
||||
const uint appID1 = 43;
|
||||
|
||||
HashSet<Asset> items = new() {
|
||||
CreateCard(1, appID0),
|
||||
CreateCard(1, appID1)
|
||||
};
|
||||
HashSet<Asset> items = [
|
||||
CreateCard(1, realAppID: appID0),
|
||||
CreateCard(1, realAppID: appID1)
|
||||
];
|
||||
|
||||
HashSet<Asset> itemsToSend = GetItemsForFullBadge(
|
||||
items, new Dictionary<uint, byte> {
|
||||
@@ -260,14 +259,14 @@ public sealed class Bot {
|
||||
const uint appID1 = 43;
|
||||
const uint appID2 = 44;
|
||||
|
||||
HashSet<Asset> items = new() {
|
||||
CreateCard(1, appID0),
|
||||
CreateCard(2, appID0),
|
||||
HashSet<Asset> items = [
|
||||
CreateCard(1, realAppID: appID0),
|
||||
CreateCard(2, realAppID: appID0),
|
||||
|
||||
CreateCard(1, appID1),
|
||||
CreateCard(2, appID1),
|
||||
CreateCard(3, appID1)
|
||||
};
|
||||
CreateCard(1, realAppID: appID1),
|
||||
CreateCard(2, realAppID: appID1),
|
||||
CreateCard(3, realAppID: appID1)
|
||||
];
|
||||
|
||||
HashSet<Asset> itemsToSend = GetItemsForFullBadge(
|
||||
items, new Dictionary<uint, byte> {
|
||||
@@ -290,10 +289,10 @@ public sealed class Bot {
|
||||
public void OtherRarityFullSets() {
|
||||
const uint appID = 42;
|
||||
|
||||
HashSet<Asset> items = new() {
|
||||
CreateCard(1, appID, rarity: Asset.ERarity.Common),
|
||||
CreateCard(1, appID, rarity: Asset.ERarity.Rare)
|
||||
};
|
||||
HashSet<Asset> items = [
|
||||
CreateCard(1, realAppID: appID, rarity: EAssetRarity.Common),
|
||||
CreateCard(1, realAppID: appID, rarity: EAssetRarity.Rare)
|
||||
];
|
||||
|
||||
HashSet<Asset> itemsToSend = GetItemsForFullBadge(items, 1, appID);
|
||||
|
||||
@@ -308,10 +307,10 @@ public sealed class Bot {
|
||||
public void OtherRarityNoSets() {
|
||||
const uint appID = 42;
|
||||
|
||||
HashSet<Asset> items = new() {
|
||||
CreateCard(1, appID, rarity: Asset.ERarity.Common),
|
||||
CreateCard(1, appID, rarity: Asset.ERarity.Rare)
|
||||
};
|
||||
HashSet<Asset> items = [
|
||||
CreateCard(1, realAppID: appID, rarity: EAssetRarity.Common),
|
||||
CreateCard(1, realAppID: appID, rarity: EAssetRarity.Rare)
|
||||
];
|
||||
|
||||
HashSet<Asset> itemsToSend = GetItemsForFullBadge(items, 2, appID);
|
||||
|
||||
@@ -324,13 +323,13 @@ public sealed class Bot {
|
||||
public void OtherRarityOneSet() {
|
||||
const uint appID = 42;
|
||||
|
||||
HashSet<Asset> items = new() {
|
||||
CreateCard(1, appID, rarity: Asset.ERarity.Common),
|
||||
CreateCard(2, appID, rarity: Asset.ERarity.Common),
|
||||
CreateCard(1, appID, rarity: Asset.ERarity.Uncommon),
|
||||
CreateCard(2, appID, rarity: Asset.ERarity.Uncommon),
|
||||
CreateCard(3, appID, rarity: Asset.ERarity.Uncommon)
|
||||
};
|
||||
HashSet<Asset> items = [
|
||||
CreateCard(1, realAppID: appID, rarity: EAssetRarity.Common),
|
||||
CreateCard(2, realAppID: appID, rarity: EAssetRarity.Common),
|
||||
CreateCard(1, realAppID: appID, rarity: EAssetRarity.Uncommon),
|
||||
CreateCard(2, realAppID: appID, rarity: EAssetRarity.Uncommon),
|
||||
CreateCard(3, realAppID: appID, rarity: EAssetRarity.Uncommon)
|
||||
];
|
||||
|
||||
HashSet<Asset> itemsToSend = GetItemsForFullBadge(items, 3, appID);
|
||||
|
||||
@@ -347,10 +346,10 @@ public sealed class Bot {
|
||||
public void OtherTypeFullSets() {
|
||||
const uint appID = 42;
|
||||
|
||||
HashSet<Asset> items = new() {
|
||||
CreateCard(1, appID, type: Asset.EType.TradingCard),
|
||||
CreateCard(1, appID, type: Asset.EType.FoilTradingCard)
|
||||
};
|
||||
HashSet<Asset> items = [
|
||||
CreateCard(1, realAppID: appID, type: EAssetType.TradingCard),
|
||||
CreateCard(1, realAppID: appID, type: EAssetType.FoilTradingCard)
|
||||
];
|
||||
|
||||
HashSet<Asset> itemsToSend = GetItemsForFullBadge(items, 1, appID);
|
||||
|
||||
@@ -365,10 +364,10 @@ public sealed class Bot {
|
||||
public void OtherTypeNoSets() {
|
||||
const uint appID = 42;
|
||||
|
||||
HashSet<Asset> items = new() {
|
||||
CreateCard(1, appID, type: Asset.EType.TradingCard),
|
||||
CreateCard(1, appID, type: Asset.EType.FoilTradingCard)
|
||||
};
|
||||
HashSet<Asset> items = [
|
||||
CreateCard(1, realAppID: appID, type: EAssetType.TradingCard),
|
||||
CreateCard(1, realAppID: appID, type: EAssetType.FoilTradingCard)
|
||||
];
|
||||
|
||||
HashSet<Asset> itemsToSend = GetItemsForFullBadge(items, 2, appID);
|
||||
|
||||
@@ -381,13 +380,13 @@ public sealed class Bot {
|
||||
public void OtherTypeOneSet() {
|
||||
const uint appID = 42;
|
||||
|
||||
HashSet<Asset> items = new() {
|
||||
CreateCard(1, appID, type: Asset.EType.TradingCard),
|
||||
CreateCard(2, appID, type: Asset.EType.TradingCard),
|
||||
CreateCard(1, appID, type: Asset.EType.FoilTradingCard),
|
||||
CreateCard(2, appID, type: Asset.EType.FoilTradingCard),
|
||||
CreateCard(3, appID, type: Asset.EType.FoilTradingCard)
|
||||
};
|
||||
HashSet<Asset> items = [
|
||||
CreateCard(1, realAppID: appID, type: EAssetType.TradingCard),
|
||||
CreateCard(2, realAppID: appID, type: EAssetType.TradingCard),
|
||||
CreateCard(1, realAppID: appID, type: EAssetType.FoilTradingCard),
|
||||
CreateCard(2, realAppID: appID, type: EAssetType.FoilTradingCard),
|
||||
CreateCard(3, realAppID: appID, type: EAssetType.FoilTradingCard)
|
||||
];
|
||||
|
||||
HashSet<Asset> itemsToSend = GetItemsForFullBadge(items, 3, appID);
|
||||
|
||||
@@ -404,10 +403,10 @@ public sealed class Bot {
|
||||
public void TooHighAmount() {
|
||||
const uint appID0 = 42;
|
||||
|
||||
HashSet<Asset> items = new() {
|
||||
CreateCard(1, appID0, 2),
|
||||
CreateCard(2, appID0)
|
||||
};
|
||||
HashSet<Asset> items = [
|
||||
CreateCard(1, amount: 2, realAppID: appID0),
|
||||
CreateCard(2, realAppID: appID0)
|
||||
];
|
||||
|
||||
HashSet<Asset> itemsToSend = GetItemsForFullBadge(items, 2, appID0);
|
||||
|
||||
@@ -423,11 +422,11 @@ public sealed class Bot {
|
||||
public void TooManyCardsForSingleTrade() {
|
||||
const uint appID = 42;
|
||||
|
||||
HashSet<Asset> items = new();
|
||||
HashSet<Asset> items = [];
|
||||
|
||||
for (byte i = 0; i < Steam.Exchange.Trading.MaxItemsPerTrade; i++) {
|
||||
items.Add(CreateCard(1, appID));
|
||||
items.Add(CreateCard(2, appID));
|
||||
items.Add(CreateCard(1, realAppID: appID));
|
||||
items.Add(CreateCard(2, realAppID: appID));
|
||||
}
|
||||
|
||||
HashSet<Asset> itemsToSend = GetItemsForFullBadge(items, 2, appID);
|
||||
@@ -440,13 +439,13 @@ public sealed class Bot {
|
||||
const uint appID0 = 42;
|
||||
const uint appID1 = 43;
|
||||
|
||||
HashSet<Asset> items = new();
|
||||
HashSet<Asset> items = [];
|
||||
|
||||
for (byte i = 0; i < 100; i++) {
|
||||
items.Add(CreateCard(1, appID0));
|
||||
items.Add(CreateCard(2, appID0));
|
||||
items.Add(CreateCard(1, appID1));
|
||||
items.Add(CreateCard(2, appID1));
|
||||
items.Add(CreateCard(1, realAppID: appID0));
|
||||
items.Add(CreateCard(2, realAppID: appID0));
|
||||
items.Add(CreateCard(1, realAppID: appID1));
|
||||
items.Add(CreateCard(2, realAppID: appID1));
|
||||
}
|
||||
|
||||
Dictionary<uint, byte> itemsPerSet = new() {
|
||||
@@ -460,28 +459,27 @@ public sealed class Bot {
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
[ExpectedException(typeof(InvalidOperationException))]
|
||||
public void TooManyCardsPerSet() {
|
||||
const uint appID0 = 42;
|
||||
const uint appID1 = 43;
|
||||
const uint appID2 = 44;
|
||||
|
||||
HashSet<Asset> items = new() {
|
||||
CreateCard(1, appID0),
|
||||
CreateCard(2, appID0),
|
||||
CreateCard(3, appID0),
|
||||
CreateCard(4, appID0)
|
||||
};
|
||||
HashSet<Asset> items = [
|
||||
CreateCard(1, realAppID: appID0),
|
||||
CreateCard(2, realAppID: appID0),
|
||||
CreateCard(3, realAppID: appID0),
|
||||
CreateCard(4, realAppID: appID0)
|
||||
];
|
||||
|
||||
GetItemsForFullBadge(
|
||||
items, new Dictionary<uint, byte> {
|
||||
{ appID0, 3 },
|
||||
{ appID1, 3 },
|
||||
{ appID2, 3 }
|
||||
}
|
||||
Assert.ThrowsException<InvalidOperationException>(
|
||||
() => GetItemsForFullBadge(
|
||||
items, new Dictionary<uint, byte> {
|
||||
{ appID0, 3 },
|
||||
{ appID1, 3 },
|
||||
{ appID2, 3 }
|
||||
}
|
||||
)
|
||||
);
|
||||
|
||||
Assert.Fail();
|
||||
}
|
||||
|
||||
private static void AssertResultMatchesExpectation(IReadOnlyDictionary<(uint RealAppID, ulong ContextID, ulong ClassID), uint> expectedResult, IReadOnlyCollection<Asset> itemsToSend) {
|
||||
@@ -493,12 +491,12 @@ public sealed class Bot {
|
||||
Assert.IsTrue(expectedResult.All(expectation => realResult.TryGetValue(expectation.Key, out long reality) && (expectation.Value == reality)));
|
||||
}
|
||||
|
||||
private static Asset CreateCard(ulong classID, uint realAppID, uint amount = 1, Asset.EType type = Asset.EType.TradingCard, Asset.ERarity rarity = Asset.ERarity.Common) => new(Asset.SteamAppID, Asset.SteamCommunityContextID, classID, amount, realAppID: realAppID, type: type, rarity: rarity);
|
||||
private static Asset CreateCard(ulong classID, ulong instanceID = 0, uint amount = 1, bool marketable = false, bool tradable = false, uint realAppID = Asset.SteamAppID, EAssetType type = EAssetType.TradingCard, EAssetRarity rarity = EAssetRarity.Common) => new(Asset.SteamAppID, Asset.SteamCommunityContextID, classID, amount, new InventoryDescription(Asset.SteamAppID, classID, instanceID, marketable, tradable, realAppID, type, rarity));
|
||||
|
||||
private static HashSet<Asset> GetItemsForFullBadge(IReadOnlyCollection<Asset> inventory, byte cardsPerSet, uint appID, ushort maxItems = Steam.Exchange.Trading.MaxItemsPerTrade) => GetItemsForFullBadge(inventory, new Dictionary<uint, byte> { { appID, cardsPerSet } }, maxItems);
|
||||
|
||||
private static HashSet<Asset> GetItemsForFullBadge(IReadOnlyCollection<Asset> inventory, IDictionary<uint, byte> cardsPerSet, ushort maxItems = Steam.Exchange.Trading.MaxItemsPerTrade) {
|
||||
Dictionary<(uint RealAppID, Asset.EType Type, Asset.ERarity Rarity), List<uint>> inventorySets = Steam.Exchange.Trading.GetInventorySets(inventory);
|
||||
Dictionary<(uint RealAppID, EAssetType Type, EAssetRarity Rarity), List<uint>> inventorySets = Steam.Exchange.Trading.GetInventorySets(inventory);
|
||||
|
||||
return GetItemsForFullSets(inventory, inventorySets.ToDictionary(static kv => kv.Key, kv => (SetsToExtract: inventorySets[kv.Key][0], cardsPerSet[kv.Key.RealAppID])), maxItems).ToHashSet();
|
||||
}
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
// ----------------------------------------------------------------------------------------------
|
||||
// _ _ _ ____ _ _____
|
||||
// / \ _ __ ___ | |__ (_)/ ___| | |_ ___ __ _ _ __ ___ | ___|__ _ _ __ _ __ ___
|
||||
// / _ \ | '__|/ __|| '_ \ | |\___ \ | __|/ _ \ / _` || '_ ` _ \ | |_ / _` || '__|| '_ ` _ \
|
||||
// / ___ \ | | | (__ | | | || | ___) || |_| __/| (_| || | | | | || _|| (_| || | | | | | | |
|
||||
// /_/ \_\|_| \___||_| |_||_||____/ \__|\___| \__,_||_| |_| |_||_| \__,_||_| |_| |_| |_|
|
||||
// ----------------------------------------------------------------------------------------------
|
||||
// |
|
||||
// Copyright 2015-2023 Łukasz "JustArchi" Domeradzki
|
||||
// Copyright 2015-2024 Łukasz "JustArchi" Domeradzki
|
||||
// Contact: JustArchi@JustArchi.net
|
||||
// |
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
@@ -200,60 +202,62 @@ public sealed class SteamChatMessage {
|
||||
public async Task RyzhehvostInitialTestForSplitting() {
|
||||
const string prefix = "/me ";
|
||||
|
||||
const string message = @"<XLimited5> Уже имеет: app/1493800 | Aircraft Carrier Survival: Prolouge
|
||||
<XLimited5> Уже имеет: app/349520 | Armillo
|
||||
<XLimited5> Уже имеет: app/346330 | BrainBread 2
|
||||
<XLimited5> Уже имеет: app/1086690 | C-War 2
|
||||
<XLimited5> Уже имеет: app/730 | Counter-Strike: Global Offensive
|
||||
<XLimited5> Уже имеет: app/838380 | DEAD OR ALIVE 6
|
||||
<XLimited5> Уже имеет: app/582890 | Estranged: The Departure
|
||||
<XLimited5> Уже имеет: app/331470 | Everlasting Summer
|
||||
<XLimited5> Уже имеет: app/1078000 | Gamecraft
|
||||
<XLimited5> Уже имеет: app/266310 | GameGuru
|
||||
<XLimited5> Уже имеет: app/275390 | Guacamelee! Super Turbo Championship Edition
|
||||
<XLimited5> Уже имеет: app/627690 | Idle Champions of the Forgotten Realms
|
||||
<XLimited5> Уже имеет: app/1048540 | Kao the Kangaroo: Round 2
|
||||
<XLimited5> Уже имеет: app/370910 | Kathy Rain
|
||||
<XLimited5> Уже имеет: app/343710 | KHOLAT
|
||||
<XLimited5> Уже имеет: app/253900 | Knights and Merchants
|
||||
<XLimited5> Уже имеет: app/224260 | No More Room in Hell
|
||||
<XLimited5> Уже имеет: app/343360 | Particula
|
||||
<XLimited5> Уже имеет: app/237870 | Planet Explorers
|
||||
<XLimited5> Уже имеет: app/684680 | Polygoneer
|
||||
<XLimited5> Уже имеет: app/1089130 | Quake II RTX
|
||||
<XLimited5> Уже имеет: app/755790 | Ring of Elysium
|
||||
<XLimited5> Уже имеет: app/1258080 | Shop Titans
|
||||
<XLimited5> Уже имеет: app/759530 | Struckd - 3D Game Creator
|
||||
<XLimited5> Уже имеет: app/269710 | Tumblestone
|
||||
<XLimited5> Уже имеет: app/304930 | Unturned
|
||||
<XLimited5> Уже имеет: app/1019250 | WWII TCG - World War 2: The Card Game
|
||||
const string message = """
|
||||
<XLimited5> Уже имеет: app/1493800 | Aircraft Carrier Survival: Prolouge
|
||||
<XLimited5> Уже имеет: app/349520 | Armillo
|
||||
<XLimited5> Уже имеет: app/346330 | BrainBread 2
|
||||
<XLimited5> Уже имеет: app/1086690 | C-War 2
|
||||
<XLimited5> Уже имеет: app/730 | Counter-Strike: Global Offensive
|
||||
<XLimited5> Уже имеет: app/838380 | DEAD OR ALIVE 6
|
||||
<XLimited5> Уже имеет: app/582890 | Estranged: The Departure
|
||||
<XLimited5> Уже имеет: app/331470 | Everlasting Summer
|
||||
<XLimited5> Уже имеет: app/1078000 | Gamecraft
|
||||
<XLimited5> Уже имеет: app/266310 | GameGuru
|
||||
<XLimited5> Уже имеет: app/275390 | Guacamelee! Super Turbo Championship Edition
|
||||
<XLimited5> Уже имеет: app/627690 | Idle Champions of the Forgotten Realms
|
||||
<XLimited5> Уже имеет: app/1048540 | Kao the Kangaroo: Round 2
|
||||
<XLimited5> Уже имеет: app/370910 | Kathy Rain
|
||||
<XLimited5> Уже имеет: app/343710 | KHOLAT
|
||||
<XLimited5> Уже имеет: app/253900 | Knights and Merchants
|
||||
<XLimited5> Уже имеет: app/224260 | No More Room in Hell
|
||||
<XLimited5> Уже имеет: app/343360 | Particula
|
||||
<XLimited5> Уже имеет: app/237870 | Planet Explorers
|
||||
<XLimited5> Уже имеет: app/684680 | Polygoneer
|
||||
<XLimited5> Уже имеет: app/1089130 | Quake II RTX
|
||||
<XLimited5> Уже имеет: app/755790 | Ring of Elysium
|
||||
<XLimited5> Уже имеет: app/1258080 | Shop Titans
|
||||
<XLimited5> Уже имеет: app/759530 | Struckd - 3D Game Creator
|
||||
<XLimited5> Уже имеет: app/269710 | Tumblestone
|
||||
<XLimited5> Уже имеет: app/304930 | Unturned
|
||||
<XLimited5> Уже имеет: app/1019250 | WWII TCG - World War 2: The Card Game
|
||||
|
||||
<ASF> 1/1 ботов уже имеют игру app/1493800 | Aircraft Carrier Survival: Prolouge.
|
||||
<ASF> 1/1 ботов уже имеют игру app/349520 | Armillo.
|
||||
<ASF> 1/1 ботов уже имеют игру app/346330 | BrainBread 2.
|
||||
<ASF> 1/1 ботов уже имеют игру app/1086690 | C-War 2.
|
||||
<ASF> 1/1 ботов уже имеют игру app/730 | Counter-Strike: Global Offensive.
|
||||
<ASF> 1/1 ботов уже имеют игру app/838380 | DEAD OR ALIVE 6.
|
||||
<ASF> 1/1 ботов уже имеют игру app/582890 | Estranged: The Departure.
|
||||
<ASF> 1/1 ботов уже имеют игру app/331470 | Everlasting Summer.
|
||||
<ASF> 1/1 ботов уже имеют игру app/1078000 | Gamecraft.
|
||||
<ASF> 1/1 ботов уже имеют игру app/266310 | GameGuru.
|
||||
<ASF> 1/1 ботов уже имеют игру app/275390 | Guacamelee! Super Turbo Championship Edition.
|
||||
<ASF> 1/1 ботов уже имеют игру app/627690 | Idle Champions of the Forgotten Realms.
|
||||
<ASF> 1/1 ботов уже имеют игру app/1048540 | Kao the Kangaroo: Round 2.
|
||||
<ASF> 1/1 ботов уже имеют игру app/370910 | Kathy Rain.
|
||||
<ASF> 1/1 ботов уже имеют игру app/343710 | KHOLAT.
|
||||
<ASF> 1/1 ботов уже имеют игру app/253900 | Knights and Merchants.
|
||||
<ASF> 1/1 ботов уже имеют игру app/224260 | No More Room in Hell.
|
||||
<ASF> 1/1 ботов уже имеют игру app/343360 | Particula.
|
||||
<ASF> 1/1 ботов уже имеют игру app/237870 | Planet Explorers.
|
||||
<ASF> 1/1 ботов уже имеют игру app/684680 | Polygoneer.
|
||||
<ASF> 1/1 ботов уже имеют игру app/1089130 | Quake II RTX.
|
||||
<ASF> 1/1 ботов уже имеют игру app/755790 | Ring of Elysium.
|
||||
<ASF> 1/1 ботов уже имеют игру app/1258080 | Shop Titans.
|
||||
<ASF> 1/1 ботов уже имеют игру app/759530 | Struckd - 3D Game Creator.
|
||||
<ASF> 1/1 ботов уже имеют игру app/269710 | Tumblestone.
|
||||
<ASF> 1/1 ботов уже имеют игру app/304930 | Unturned.";
|
||||
<ASF> 1/1 ботов уже имеют игру app/1493800 | Aircraft Carrier Survival: Prolouge.
|
||||
<ASF> 1/1 ботов уже имеют игру app/349520 | Armillo.
|
||||
<ASF> 1/1 ботов уже имеют игру app/346330 | BrainBread 2.
|
||||
<ASF> 1/1 ботов уже имеют игру app/1086690 | C-War 2.
|
||||
<ASF> 1/1 ботов уже имеют игру app/730 | Counter-Strike: Global Offensive.
|
||||
<ASF> 1/1 ботов уже имеют игру app/838380 | DEAD OR ALIVE 6.
|
||||
<ASF> 1/1 ботов уже имеют игру app/582890 | Estranged: The Departure.
|
||||
<ASF> 1/1 ботов уже имеют игру app/331470 | Everlasting Summer.
|
||||
<ASF> 1/1 ботов уже имеют игру app/1078000 | Gamecraft.
|
||||
<ASF> 1/1 ботов уже имеют игру app/266310 | GameGuru.
|
||||
<ASF> 1/1 ботов уже имеют игру app/275390 | Guacamelee! Super Turbo Championship Edition.
|
||||
<ASF> 1/1 ботов уже имеют игру app/627690 | Idle Champions of the Forgotten Realms.
|
||||
<ASF> 1/1 ботов уже имеют игру app/1048540 | Kao the Kangaroo: Round 2.
|
||||
<ASF> 1/1 ботов уже имеют игру app/370910 | Kathy Rain.
|
||||
<ASF> 1/1 ботов уже имеют игру app/343710 | KHOLAT.
|
||||
<ASF> 1/1 ботов уже имеют игру app/253900 | Knights and Merchants.
|
||||
<ASF> 1/1 ботов уже имеют игру app/224260 | No More Room in Hell.
|
||||
<ASF> 1/1 ботов уже имеют игру app/343360 | Particula.
|
||||
<ASF> 1/1 ботов уже имеют игру app/237870 | Planet Explorers.
|
||||
<ASF> 1/1 ботов уже имеют игру app/684680 | Polygoneer.
|
||||
<ASF> 1/1 ботов уже имеют игру app/1089130 | Quake II RTX.
|
||||
<ASF> 1/1 ботов уже имеют игру app/755790 | Ring of Elysium.
|
||||
<ASF> 1/1 ботов уже имеют игру app/1258080 | Shop Titans.
|
||||
<ASF> 1/1 ботов уже имеют игру app/759530 | Struckd - 3D Game Creator.
|
||||
<ASF> 1/1 ботов уже имеют игру app/269710 | Tumblestone.
|
||||
<ASF> 1/1 ботов уже имеют игру app/304930 | Unturned.
|
||||
""";
|
||||
|
||||
List<string> output = await GetMessageParts(message, prefix).ToListAsync().ConfigureAwait(false);
|
||||
|
||||
@@ -309,27 +313,21 @@ public sealed class SteamChatMessage {
|
||||
Assert.AreEqual(newlinePart, output[3]);
|
||||
}
|
||||
|
||||
[ExpectedException(typeof(ArgumentOutOfRangeException))]
|
||||
[TestMethod]
|
||||
public async Task ThrowsOnTooLongNewlinesPrefix() {
|
||||
string prefix = new('\n', (MaxMessagePrefixBytes / NewlineWeight) + 1);
|
||||
|
||||
const string message = "asdf";
|
||||
|
||||
await GetMessageParts(message, prefix).ToListAsync().ConfigureAwait(false);
|
||||
|
||||
Assert.Fail();
|
||||
await Assert.ThrowsExceptionAsync<ArgumentOutOfRangeException>(async () => await GetMessageParts(message, prefix).ToListAsync().ConfigureAwait(false)).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
[ExpectedException(typeof(ArgumentOutOfRangeException))]
|
||||
[TestMethod]
|
||||
public async Task ThrowsOnTooLongPrefix() {
|
||||
string prefix = new('x', MaxMessagePrefixBytes + 1);
|
||||
|
||||
const string message = "asdf";
|
||||
|
||||
await GetMessageParts(message, prefix).ToListAsync().ConfigureAwait(false);
|
||||
|
||||
Assert.Fail();
|
||||
await Assert.ThrowsExceptionAsync<ArgumentOutOfRangeException>(async () => await GetMessageParts(message, prefix).ToListAsync().ConfigureAwait(false)).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
// ----------------------------------------------------------------------------------------------
|
||||
// _ _ _ ____ _ _____
|
||||
// / \ _ __ ___ | |__ (_)/ ___| | |_ ___ __ _ _ __ ___ | ___|__ _ _ __ _ __ ___
|
||||
// / _ \ | '__|/ __|| '_ \ | |\___ \ | __|/ _ \ / _` || '_ ` _ \ | |_ / _` || '__|| '_ ` _ \
|
||||
// / ___ \ | | | (__ | | | || | ___) || |_| __/| (_| || | | | | || _|| (_| || | | | | | | |
|
||||
// /_/ \_\|_| \___||_| |_||_||____/ \__|\___| \__,_||_| |_| |_||_| \__,_||_| |_| |_| |_|
|
||||
// ----------------------------------------------------------------------------------------------
|
||||
// |
|
||||
// Copyright 2015-2023 Łukasz "JustArchi" Domeradzki
|
||||
// Copyright 2015-2024 Łukasz "JustArchi" Domeradzki
|
||||
// Contact: JustArchi@JustArchi.net
|
||||
// |
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
@@ -30,21 +32,21 @@ namespace ArchiSteamFarm.Tests;
|
||||
public sealed class Trading {
|
||||
[TestMethod]
|
||||
public void ExploitingNewSetsIsFairButNotNeutral() {
|
||||
HashSet<Asset> inventory = new() {
|
||||
CreateItem(1, 40),
|
||||
CreateItem(2, 10),
|
||||
CreateItem(3, 10)
|
||||
};
|
||||
HashSet<Asset> inventory = [
|
||||
CreateItem(1, amount: 40),
|
||||
CreateItem(2, amount: 10),
|
||||
CreateItem(3, amount: 10)
|
||||
];
|
||||
|
||||
HashSet<Asset> itemsToGive = new() {
|
||||
CreateItem(2, 5),
|
||||
CreateItem(3, 5)
|
||||
};
|
||||
HashSet<Asset> itemsToGive = [
|
||||
CreateItem(2, amount: 5),
|
||||
CreateItem(3, amount: 5)
|
||||
];
|
||||
|
||||
HashSet<Asset> itemsToReceive = new() {
|
||||
CreateItem(1, 9),
|
||||
HashSet<Asset> itemsToReceive = [
|
||||
CreateItem(1, amount: 9),
|
||||
CreateItem(4)
|
||||
};
|
||||
];
|
||||
|
||||
Assert.IsTrue(IsFairExchange(itemsToGive, itemsToReceive));
|
||||
Assert.IsFalse(IsTradeNeutralOrBetter(inventory, itemsToGive, itemsToReceive));
|
||||
@@ -52,45 +54,60 @@ public sealed class Trading {
|
||||
|
||||
[TestMethod]
|
||||
public void MismatchRarityIsNotFair() {
|
||||
HashSet<Asset> itemsToGive = new() { CreateItem(1, rarity: Asset.ERarity.Rare) };
|
||||
HashSet<Asset> itemsToReceive = new() { CreateItem(2) };
|
||||
HashSet<Asset> itemsToGive = [
|
||||
CreateItem(1, rarity: EAssetRarity.Rare)
|
||||
];
|
||||
|
||||
HashSet<Asset> itemsToReceive = [
|
||||
CreateItem(2)
|
||||
];
|
||||
|
||||
Assert.IsFalse(IsFairExchange(itemsToGive, itemsToReceive));
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void MismatchRealAppIDsIsNotFair() {
|
||||
HashSet<Asset> itemsToGive = new() { CreateItem(1, realAppID: 570) };
|
||||
HashSet<Asset> itemsToReceive = new() { CreateItem(2) };
|
||||
HashSet<Asset> itemsToGive = [
|
||||
CreateItem(1, realAppID: 570)
|
||||
];
|
||||
|
||||
HashSet<Asset> itemsToReceive = [
|
||||
CreateItem(2)
|
||||
];
|
||||
|
||||
Assert.IsFalse(IsFairExchange(itemsToGive, itemsToReceive));
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void MismatchTypesIsNotFair() {
|
||||
HashSet<Asset> itemsToGive = new() { CreateItem(1, type: Asset.EType.Emoticon) };
|
||||
HashSet<Asset> itemsToReceive = new() { CreateItem(2) };
|
||||
HashSet<Asset> itemsToGive = [
|
||||
CreateItem(1, type: EAssetType.Emoticon)
|
||||
];
|
||||
|
||||
HashSet<Asset> itemsToReceive = [
|
||||
CreateItem(2)
|
||||
];
|
||||
|
||||
Assert.IsFalse(IsFairExchange(itemsToGive, itemsToReceive));
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void MultiGameMultiTypeBadReject() {
|
||||
HashSet<Asset> inventory = new() {
|
||||
CreateItem(1, 9),
|
||||
CreateItem(3, 9, 730, Asset.EType.Emoticon),
|
||||
CreateItem(4, realAppID: 730, type: Asset.EType.Emoticon)
|
||||
};
|
||||
HashSet<Asset> inventory = [
|
||||
CreateItem(1, amount: 9),
|
||||
CreateItem(3, amount: 9, realAppID: 730, type: EAssetType.Emoticon),
|
||||
CreateItem(4, realAppID: 730, type: EAssetType.Emoticon)
|
||||
];
|
||||
|
||||
HashSet<Asset> itemsToGive = new() {
|
||||
HashSet<Asset> itemsToGive = [
|
||||
CreateItem(1),
|
||||
CreateItem(4, realAppID: 730, type: Asset.EType.Emoticon)
|
||||
};
|
||||
CreateItem(4, realAppID: 730, type: EAssetType.Emoticon)
|
||||
];
|
||||
|
||||
HashSet<Asset> itemsToReceive = new() {
|
||||
HashSet<Asset> itemsToReceive = [
|
||||
CreateItem(2),
|
||||
CreateItem(3, realAppID: 730, type: Asset.EType.Emoticon)
|
||||
};
|
||||
CreateItem(3, realAppID: 730, type: EAssetType.Emoticon)
|
||||
];
|
||||
|
||||
Assert.IsTrue(IsFairExchange(itemsToGive, itemsToReceive));
|
||||
Assert.IsFalse(IsTradeNeutralOrBetter(inventory, itemsToGive, itemsToReceive));
|
||||
@@ -98,20 +115,20 @@ public sealed class Trading {
|
||||
|
||||
[TestMethod]
|
||||
public void MultiGameMultiTypeNeutralAccept() {
|
||||
HashSet<Asset> inventory = new() {
|
||||
CreateItem(1, 9),
|
||||
CreateItem(3, realAppID: 730, type: Asset.EType.Emoticon)
|
||||
};
|
||||
HashSet<Asset> inventory = [
|
||||
CreateItem(1, amount: 9),
|
||||
CreateItem(3, realAppID: 730, type: EAssetType.Emoticon)
|
||||
];
|
||||
|
||||
HashSet<Asset> itemsToGive = new() {
|
||||
HashSet<Asset> itemsToGive = [
|
||||
CreateItem(1),
|
||||
CreateItem(3, realAppID: 730, type: Asset.EType.Emoticon)
|
||||
};
|
||||
CreateItem(3, realAppID: 730, type: EAssetType.Emoticon)
|
||||
];
|
||||
|
||||
HashSet<Asset> itemsToReceive = new() {
|
||||
HashSet<Asset> itemsToReceive = [
|
||||
CreateItem(2),
|
||||
CreateItem(4, realAppID: 730, type: Asset.EType.Emoticon)
|
||||
};
|
||||
CreateItem(4, realAppID: 730, type: EAssetType.Emoticon)
|
||||
];
|
||||
|
||||
Assert.IsTrue(IsFairExchange(itemsToGive, itemsToReceive));
|
||||
Assert.IsTrue(IsTradeNeutralOrBetter(inventory, itemsToGive, itemsToReceive));
|
||||
@@ -119,21 +136,21 @@ public sealed class Trading {
|
||||
|
||||
[TestMethod]
|
||||
public void MultiGameSingleTypeBadReject() {
|
||||
HashSet<Asset> inventory = new() {
|
||||
CreateItem(1, 9),
|
||||
HashSet<Asset> inventory = [
|
||||
CreateItem(1, amount: 9),
|
||||
CreateItem(3, realAppID: 730),
|
||||
CreateItem(4, realAppID: 730)
|
||||
};
|
||||
];
|
||||
|
||||
HashSet<Asset> itemsToGive = new() {
|
||||
HashSet<Asset> itemsToGive = [
|
||||
CreateItem(1),
|
||||
CreateItem(3, realAppID: 730)
|
||||
};
|
||||
];
|
||||
|
||||
HashSet<Asset> itemsToReceive = new() {
|
||||
HashSet<Asset> itemsToReceive = [
|
||||
CreateItem(2),
|
||||
CreateItem(4, realAppID: 730)
|
||||
};
|
||||
];
|
||||
|
||||
Assert.IsTrue(IsFairExchange(itemsToGive, itemsToReceive));
|
||||
Assert.IsFalse(IsTradeNeutralOrBetter(inventory, itemsToGive, itemsToReceive));
|
||||
@@ -141,20 +158,20 @@ public sealed class Trading {
|
||||
|
||||
[TestMethod]
|
||||
public void MultiGameSingleTypeNeutralAccept() {
|
||||
HashSet<Asset> inventory = new() {
|
||||
CreateItem(1, 2),
|
||||
HashSet<Asset> inventory = [
|
||||
CreateItem(1, amount: 2),
|
||||
CreateItem(3, realAppID: 730)
|
||||
};
|
||||
];
|
||||
|
||||
HashSet<Asset> itemsToGive = new() {
|
||||
HashSet<Asset> itemsToGive = [
|
||||
CreateItem(1),
|
||||
CreateItem(3, realAppID: 730)
|
||||
};
|
||||
];
|
||||
|
||||
HashSet<Asset> itemsToReceive = new() {
|
||||
HashSet<Asset> itemsToReceive = [
|
||||
CreateItem(2),
|
||||
CreateItem(4, realAppID: 730)
|
||||
};
|
||||
];
|
||||
|
||||
Assert.IsTrue(IsFairExchange(itemsToGive, itemsToReceive));
|
||||
Assert.IsTrue(IsTradeNeutralOrBetter(inventory, itemsToGive, itemsToReceive));
|
||||
@@ -162,21 +179,21 @@ public sealed class Trading {
|
||||
|
||||
[TestMethod]
|
||||
public void SingleGameAbrynosWasWrongNeutralAccept() {
|
||||
HashSet<Asset> inventory = new() {
|
||||
HashSet<Asset> inventory = [
|
||||
CreateItem(1),
|
||||
CreateItem(2, 2),
|
||||
CreateItem(2, amount: 2),
|
||||
CreateItem(3),
|
||||
CreateItem(4),
|
||||
CreateItem(5)
|
||||
};
|
||||
];
|
||||
|
||||
HashSet<Asset> itemsToGive = new() {
|
||||
HashSet<Asset> itemsToGive = [
|
||||
CreateItem(2)
|
||||
};
|
||||
];
|
||||
|
||||
HashSet<Asset> itemsToReceive = new() {
|
||||
HashSet<Asset> itemsToReceive = [
|
||||
CreateItem(3)
|
||||
};
|
||||
];
|
||||
|
||||
Assert.IsTrue(IsFairExchange(itemsToGive, itemsToReceive));
|
||||
Assert.IsTrue(IsTradeNeutralOrBetter(inventory, itemsToGive, itemsToReceive));
|
||||
@@ -184,18 +201,18 @@ public sealed class Trading {
|
||||
|
||||
[TestMethod]
|
||||
public void SingleGameDonationAccept() {
|
||||
HashSet<Asset> inventory = new() {
|
||||
HashSet<Asset> inventory = [
|
||||
CreateItem(1)
|
||||
};
|
||||
];
|
||||
|
||||
HashSet<Asset> itemsToGive = new() {
|
||||
HashSet<Asset> itemsToGive = [
|
||||
CreateItem(1)
|
||||
};
|
||||
];
|
||||
|
||||
HashSet<Asset> itemsToReceive = new() {
|
||||
HashSet<Asset> itemsToReceive = [
|
||||
CreateItem(2),
|
||||
CreateItem(3, type: Asset.EType.SteamGems)
|
||||
};
|
||||
CreateItem(3, type: EAssetType.SteamGems)
|
||||
];
|
||||
|
||||
Assert.IsTrue(IsFairExchange(itemsToGive, itemsToReceive));
|
||||
Assert.IsTrue(IsTradeNeutralOrBetter(inventory, itemsToGive, itemsToReceive));
|
||||
@@ -203,21 +220,21 @@ public sealed class Trading {
|
||||
|
||||
[TestMethod]
|
||||
public void SingleGameMultiTypeBadReject() {
|
||||
HashSet<Asset> inventory = new() {
|
||||
CreateItem(1, 9),
|
||||
CreateItem(3, 9, type: Asset.EType.Emoticon),
|
||||
CreateItem(4, type: Asset.EType.Emoticon)
|
||||
};
|
||||
HashSet<Asset> inventory = [
|
||||
CreateItem(1, amount: 9),
|
||||
CreateItem(3, amount: 9, type: EAssetType.Emoticon),
|
||||
CreateItem(4, type: EAssetType.Emoticon)
|
||||
];
|
||||
|
||||
HashSet<Asset> itemsToGive = new() {
|
||||
HashSet<Asset> itemsToGive = [
|
||||
CreateItem(1),
|
||||
CreateItem(4, type: Asset.EType.Emoticon)
|
||||
};
|
||||
CreateItem(4, type: EAssetType.Emoticon)
|
||||
];
|
||||
|
||||
HashSet<Asset> itemsToReceive = new() {
|
||||
HashSet<Asset> itemsToReceive = [
|
||||
CreateItem(2),
|
||||
CreateItem(3, type: Asset.EType.Emoticon)
|
||||
};
|
||||
CreateItem(3, type: EAssetType.Emoticon)
|
||||
];
|
||||
|
||||
Assert.IsTrue(IsFairExchange(itemsToGive, itemsToReceive));
|
||||
Assert.IsFalse(IsTradeNeutralOrBetter(inventory, itemsToGive, itemsToReceive));
|
||||
@@ -225,20 +242,20 @@ public sealed class Trading {
|
||||
|
||||
[TestMethod]
|
||||
public void SingleGameMultiTypeNeutralAccept() {
|
||||
HashSet<Asset> inventory = new() {
|
||||
CreateItem(1, 9),
|
||||
CreateItem(3, type: Asset.EType.Emoticon)
|
||||
};
|
||||
HashSet<Asset> inventory = [
|
||||
CreateItem(1, amount: 9),
|
||||
CreateItem(3, type: EAssetType.Emoticon)
|
||||
];
|
||||
|
||||
HashSet<Asset> itemsToGive = new() {
|
||||
HashSet<Asset> itemsToGive = [
|
||||
CreateItem(1),
|
||||
CreateItem(3, type: Asset.EType.Emoticon)
|
||||
};
|
||||
CreateItem(3, type: EAssetType.Emoticon)
|
||||
];
|
||||
|
||||
HashSet<Asset> itemsToReceive = new() {
|
||||
HashSet<Asset> itemsToReceive = [
|
||||
CreateItem(2),
|
||||
CreateItem(4, type: Asset.EType.Emoticon)
|
||||
};
|
||||
CreateItem(4, type: EAssetType.Emoticon)
|
||||
];
|
||||
|
||||
Assert.IsTrue(IsFairExchange(itemsToGive, itemsToReceive));
|
||||
Assert.IsTrue(IsTradeNeutralOrBetter(inventory, itemsToGive, itemsToReceive));
|
||||
@@ -246,19 +263,21 @@ public sealed class Trading {
|
||||
|
||||
[TestMethod]
|
||||
public void SingleGameQuantityBadReject() {
|
||||
HashSet<Asset> inventory = new() {
|
||||
HashSet<Asset> inventory = [
|
||||
CreateItem(1),
|
||||
CreateItem(2),
|
||||
CreateItem(3)
|
||||
};
|
||||
];
|
||||
|
||||
HashSet<Asset> itemsToGive = new() {
|
||||
HashSet<Asset> itemsToGive = [
|
||||
CreateItem(1),
|
||||
CreateItem(2),
|
||||
CreateItem(3)
|
||||
};
|
||||
];
|
||||
|
||||
HashSet<Asset> itemsToReceive = new() { CreateItem(4, 3) };
|
||||
HashSet<Asset> itemsToReceive = [
|
||||
CreateItem(4, amount: 3)
|
||||
];
|
||||
|
||||
Assert.IsTrue(IsFairExchange(itemsToGive, itemsToReceive));
|
||||
Assert.IsFalse(IsTradeNeutralOrBetter(inventory, itemsToGive, itemsToReceive));
|
||||
@@ -266,17 +285,19 @@ public sealed class Trading {
|
||||
|
||||
[TestMethod]
|
||||
public void SingleGameQuantityBadReject2() {
|
||||
HashSet<Asset> inventory = new() {
|
||||
HashSet<Asset> inventory = [
|
||||
CreateItem(1),
|
||||
CreateItem(2, 2)
|
||||
};
|
||||
CreateItem(2, amount: 2)
|
||||
];
|
||||
|
||||
HashSet<Asset> itemsToGive = new() {
|
||||
HashSet<Asset> itemsToGive = [
|
||||
CreateItem(1),
|
||||
CreateItem(2, 2)
|
||||
};
|
||||
CreateItem(2, amount: 2)
|
||||
];
|
||||
|
||||
HashSet<Asset> itemsToReceive = new() { CreateItem(3, 3) };
|
||||
HashSet<Asset> itemsToReceive = [
|
||||
CreateItem(3, amount: 3)
|
||||
];
|
||||
|
||||
Assert.IsTrue(IsFairExchange(itemsToGive, itemsToReceive));
|
||||
Assert.IsFalse(IsTradeNeutralOrBetter(inventory, itemsToGive, itemsToReceive));
|
||||
@@ -284,17 +305,19 @@ public sealed class Trading {
|
||||
|
||||
[TestMethod]
|
||||
public void SingleGameQuantityNeutralAccept() {
|
||||
HashSet<Asset> inventory = new() {
|
||||
CreateItem(1, 2),
|
||||
HashSet<Asset> inventory = [
|
||||
CreateItem(1, amount: 2),
|
||||
CreateItem(2)
|
||||
};
|
||||
];
|
||||
|
||||
HashSet<Asset> itemsToGive = new() {
|
||||
HashSet<Asset> itemsToGive = [
|
||||
CreateItem(1),
|
||||
CreateItem(2)
|
||||
};
|
||||
];
|
||||
|
||||
HashSet<Asset> itemsToReceive = new() { CreateItem(3, 2) };
|
||||
HashSet<Asset> itemsToReceive = [
|
||||
CreateItem(3, amount: 2)
|
||||
];
|
||||
|
||||
Assert.IsTrue(IsFairExchange(itemsToGive, itemsToReceive));
|
||||
Assert.IsTrue(IsTradeNeutralOrBetter(inventory, itemsToGive, itemsToReceive));
|
||||
@@ -302,13 +325,18 @@ public sealed class Trading {
|
||||
|
||||
[TestMethod]
|
||||
public void SingleGameSingleTypeBadReject() {
|
||||
HashSet<Asset> inventory = new() {
|
||||
HashSet<Asset> inventory = [
|
||||
CreateItem(1),
|
||||
CreateItem(2)
|
||||
};
|
||||
];
|
||||
|
||||
HashSet<Asset> itemsToGive = new() { CreateItem(1) };
|
||||
HashSet<Asset> itemsToReceive = new() { CreateItem(2) };
|
||||
HashSet<Asset> itemsToGive = [
|
||||
CreateItem(1)
|
||||
];
|
||||
|
||||
HashSet<Asset> itemsToReceive = [
|
||||
CreateItem(2)
|
||||
];
|
||||
|
||||
Assert.IsTrue(IsFairExchange(itemsToGive, itemsToReceive));
|
||||
Assert.IsFalse(IsTradeNeutralOrBetter(inventory, itemsToGive, itemsToReceive));
|
||||
@@ -316,18 +344,20 @@ public sealed class Trading {
|
||||
|
||||
[TestMethod]
|
||||
public void SingleGameSingleTypeBadWithOverpayingReject() {
|
||||
HashSet<Asset> inventory = new() {
|
||||
CreateItem(1, 2),
|
||||
CreateItem(2, 2),
|
||||
CreateItem(3, 2)
|
||||
};
|
||||
HashSet<Asset> inventory = [
|
||||
CreateItem(1, amount: 2),
|
||||
CreateItem(2, amount: 2),
|
||||
CreateItem(3, amount: 2)
|
||||
];
|
||||
|
||||
HashSet<Asset> itemsToGive = new() { CreateItem(2) };
|
||||
HashSet<Asset> itemsToGive = [
|
||||
CreateItem(2)
|
||||
];
|
||||
|
||||
HashSet<Asset> itemsToReceive = new() {
|
||||
HashSet<Asset> itemsToReceive = [
|
||||
CreateItem(1),
|
||||
CreateItem(3)
|
||||
};
|
||||
];
|
||||
|
||||
Assert.IsTrue(IsFairExchange(itemsToGive, itemsToReceive));
|
||||
Assert.IsFalse(IsTradeNeutralOrBetter(inventory, itemsToGive, itemsToReceive));
|
||||
@@ -335,14 +365,19 @@ public sealed class Trading {
|
||||
|
||||
[TestMethod]
|
||||
public void SingleGameSingleTypeBigDifferenceAccept() {
|
||||
HashSet<Asset> inventory = new() {
|
||||
HashSet<Asset> inventory = [
|
||||
CreateItem(1),
|
||||
CreateItem(2, 5),
|
||||
CreateItem(2, amount: 5),
|
||||
CreateItem(3)
|
||||
};
|
||||
];
|
||||
|
||||
HashSet<Asset> itemsToGive = new() { CreateItem(2) };
|
||||
HashSet<Asset> itemsToReceive = new() { CreateItem(3) };
|
||||
HashSet<Asset> itemsToGive = [
|
||||
CreateItem(2)
|
||||
];
|
||||
|
||||
HashSet<Asset> itemsToReceive = [
|
||||
CreateItem(3)
|
||||
];
|
||||
|
||||
Assert.IsTrue(IsFairExchange(itemsToGive, itemsToReceive));
|
||||
Assert.IsTrue(IsTradeNeutralOrBetter(inventory, itemsToGive, itemsToReceive));
|
||||
@@ -350,23 +385,23 @@ public sealed class Trading {
|
||||
|
||||
[TestMethod]
|
||||
public void SingleGameSingleTypeBigDifferenceReject() {
|
||||
HashSet<Asset> inventory = new() {
|
||||
HashSet<Asset> inventory = [
|
||||
CreateItem(1),
|
||||
CreateItem(2, 2),
|
||||
CreateItem(3, 2),
|
||||
CreateItem(4, 3),
|
||||
CreateItem(5, 10)
|
||||
};
|
||||
CreateItem(2, amount: 2),
|
||||
CreateItem(3, amount: 2),
|
||||
CreateItem(4, amount: 3),
|
||||
CreateItem(5, amount: 10)
|
||||
];
|
||||
|
||||
HashSet<Asset> itemsToGive = new() {
|
||||
HashSet<Asset> itemsToGive = [
|
||||
CreateItem(2),
|
||||
CreateItem(5)
|
||||
};
|
||||
];
|
||||
|
||||
HashSet<Asset> itemsToReceive = new() {
|
||||
HashSet<Asset> itemsToReceive = [
|
||||
CreateItem(3),
|
||||
CreateItem(4)
|
||||
};
|
||||
];
|
||||
|
||||
Assert.IsTrue(IsFairExchange(itemsToGive, itemsToReceive));
|
||||
Assert.IsFalse(IsTradeNeutralOrBetter(inventory, itemsToGive, itemsToReceive));
|
||||
@@ -374,9 +409,17 @@ public sealed class Trading {
|
||||
|
||||
[TestMethod]
|
||||
public void SingleGameSingleTypeGoodAccept() {
|
||||
HashSet<Asset> inventory = new() { CreateItem(1, 2) };
|
||||
HashSet<Asset> itemsToGive = new() { CreateItem(1) };
|
||||
HashSet<Asset> itemsToReceive = new() { CreateItem(2) };
|
||||
HashSet<Asset> inventory = [
|
||||
CreateItem(1, amount: 2)
|
||||
];
|
||||
|
||||
HashSet<Asset> itemsToGive = [
|
||||
CreateItem(1)
|
||||
];
|
||||
|
||||
HashSet<Asset> itemsToReceive = [
|
||||
CreateItem(2)
|
||||
];
|
||||
|
||||
Assert.IsTrue(IsFairExchange(itemsToGive, itemsToReceive));
|
||||
Assert.IsTrue(IsTradeNeutralOrBetter(inventory, itemsToGive, itemsToReceive));
|
||||
@@ -384,9 +427,17 @@ public sealed class Trading {
|
||||
|
||||
[TestMethod]
|
||||
public void SingleGameSingleTypeNeutralAccept() {
|
||||
HashSet<Asset> inventory = new() { CreateItem(1) };
|
||||
HashSet<Asset> itemsToGive = new() { CreateItem(1) };
|
||||
HashSet<Asset> itemsToReceive = new() { CreateItem(2) };
|
||||
HashSet<Asset> inventory = [
|
||||
CreateItem(1)
|
||||
];
|
||||
|
||||
HashSet<Asset> itemsToGive = [
|
||||
CreateItem(1)
|
||||
];
|
||||
|
||||
HashSet<Asset> itemsToReceive = [
|
||||
CreateItem(2)
|
||||
];
|
||||
|
||||
Assert.IsTrue(IsFairExchange(itemsToGive, itemsToReceive));
|
||||
Assert.IsTrue(IsTradeNeutralOrBetter(inventory, itemsToGive, itemsToReceive));
|
||||
@@ -394,17 +445,19 @@ public sealed class Trading {
|
||||
|
||||
[TestMethod]
|
||||
public void SingleGameSingleTypeNeutralWithOverpayingAccept() {
|
||||
HashSet<Asset> inventory = new() {
|
||||
CreateItem(1, 2),
|
||||
CreateItem(2, 2)
|
||||
};
|
||||
HashSet<Asset> inventory = [
|
||||
CreateItem(1, amount: 2),
|
||||
CreateItem(2, amount: 2)
|
||||
];
|
||||
|
||||
HashSet<Asset> itemsToGive = new() { CreateItem(2) };
|
||||
HashSet<Asset> itemsToGive = [
|
||||
CreateItem(2)
|
||||
];
|
||||
|
||||
HashSet<Asset> itemsToReceive = new() {
|
||||
HashSet<Asset> itemsToReceive = [
|
||||
CreateItem(1),
|
||||
CreateItem(3)
|
||||
};
|
||||
];
|
||||
|
||||
Assert.IsTrue(IsFairExchange(itemsToGive, itemsToReceive));
|
||||
Assert.IsTrue(IsTradeNeutralOrBetter(inventory, itemsToGive, itemsToReceive));
|
||||
@@ -412,27 +465,29 @@ public sealed class Trading {
|
||||
|
||||
[TestMethod]
|
||||
public void TakingExcessiveAmountOfSingleCardCanStillBeFairAndNeutral() {
|
||||
HashSet<Asset> inventory = new() {
|
||||
CreateItem(1, 52),
|
||||
CreateItem(2, 73),
|
||||
CreateItem(3, 52),
|
||||
CreateItem(4, 47),
|
||||
HashSet<Asset> inventory = [
|
||||
CreateItem(1, amount: 52),
|
||||
CreateItem(2, amount: 73),
|
||||
CreateItem(3, amount: 52),
|
||||
CreateItem(4, amount: 47),
|
||||
CreateItem(5)
|
||||
};
|
||||
];
|
||||
|
||||
HashSet<Asset> itemsToGive = new() { CreateItem(2, 73) };
|
||||
HashSet<Asset> itemsToGive = [
|
||||
CreateItem(2, amount: 73)
|
||||
];
|
||||
|
||||
HashSet<Asset> itemsToReceive = new() {
|
||||
CreateItem(1, 9),
|
||||
CreateItem(3, 9),
|
||||
CreateItem(4, 8),
|
||||
CreateItem(5, 24),
|
||||
CreateItem(6, 23)
|
||||
};
|
||||
HashSet<Asset> itemsToReceive = [
|
||||
CreateItem(1, amount: 9),
|
||||
CreateItem(3, amount: 9),
|
||||
CreateItem(4, amount: 8),
|
||||
CreateItem(5, amount: 24),
|
||||
CreateItem(6, amount: 23)
|
||||
];
|
||||
|
||||
Assert.IsTrue(IsFairExchange(itemsToGive, itemsToReceive));
|
||||
Assert.IsTrue(IsTradeNeutralOrBetter(inventory, itemsToGive, itemsToReceive));
|
||||
}
|
||||
|
||||
private static Asset CreateItem(ulong classID, uint amount = 1, uint realAppID = Asset.SteamAppID, Asset.EType type = Asset.EType.TradingCard, Asset.ERarity rarity = Asset.ERarity.Common) => new(Asset.SteamAppID, Asset.SteamCommunityContextID, classID, amount, realAppID: realAppID, type: type, rarity: rarity);
|
||||
private static Asset CreateItem(ulong classID, ulong instanceID = 0, uint amount = 1, bool marketable = false, bool tradable = false, uint realAppID = Asset.SteamAppID, EAssetType type = EAssetType.TradingCard, EAssetRarity rarity = EAssetRarity.Common) => new(Asset.SteamAppID, Asset.SteamCommunityContextID, classID, amount, new InventoryDescription(Asset.SteamAppID, classID, instanceID, marketable, tradable, realAppID, type, rarity));
|
||||
}
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
// _ _ _ ____ _ _____
|
||||
// ----------------------------------------------------------------------------------------------
|
||||
// _ _ _ ____ _ _____
|
||||
// / \ _ __ ___ | |__ (_)/ ___| | |_ ___ __ _ _ __ ___ | ___|__ _ _ __ _ __ ___
|
||||
// / _ \ | '__|/ __|| '_ \ | |\___ \ | __|/ _ \ / _` || '_ ` _ \ | |_ / _` || '__|| '_ ` _ \
|
||||
// / ___ \ | | | (__ | | | || | ___) || |_| __/| (_| || | | | | || _|| (_| || | | | | | | |
|
||||
// /_/ \_\|_| \___||_| |_||_||____/ \__|\___| \__,_||_| |_| |_||_| \__,_||_| |_| |_| |_|
|
||||
// ----------------------------------------------------------------------------------------------
|
||||
// |
|
||||
// Copyright 2015-2023 Łukasz "JustArchi" Domeradzki
|
||||
// Copyright 2015-2024 Łukasz "JustArchi" Domeradzki
|
||||
// Contact: JustArchi@JustArchi.net
|
||||
// |
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -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>
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
// ----------------------------------------------------------------------------------------------
|
||||
// _ _ _ ____ _ _____
|
||||
// / \ _ __ ___ | |__ (_)/ ___| | |_ ___ __ _ _ __ ___ | ___|__ _ _ __ _ __ ___
|
||||
// / _ \ | '__|/ __|| '_ \ | |\___ \ | __|/ _ \ / _` || '_ ` _ \ | |_ / _` || '__|| '_ ` _ \
|
||||
// / ___ \ | | | (__ | | | || | ___) || |_| __/| (_| || | | | | || _|| (_| || | | | | | | |
|
||||
// /_/ \_\|_| \___||_| |_||_||____/ \__|\___| \__,_||_| |_| |_||_| \__,_||_| |_| |_| |_|
|
||||
// ----------------------------------------------------------------------------------------------
|
||||
// |
|
||||
// Copyright 2015-2023 Łukasz "JustArchi" Domeradzki
|
||||
// Copyright 2015-2024 Łukasz "JustArchi" Domeradzki
|
||||
// Contact: JustArchi@JustArchi.net
|
||||
// |
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
// ----------------------------------------------------------------------------------------------
|
||||
// _ _ _ ____ _ _____
|
||||
// / \ _ __ ___ | |__ (_)/ ___| | |_ ___ __ _ _ __ ___ | ___|__ _ _ __ _ __ ___
|
||||
// / _ \ | '__|/ __|| '_ \ | |\___ \ | __|/ _ \ / _` || '_ ` _ \ | |_ / _` || '__|| '_ ` _ \
|
||||
// / ___ \ | | | (__ | | | || | ___) || |_| __/| (_| || | | | | || _|| (_| || | | | | | | |
|
||||
// /_/ \_\|_| \___||_| |_||_||____/ \__|\___| \__,_||_| |_| |_||_| \__,_||_| |_| |_| |_|
|
||||
// ----------------------------------------------------------------------------------------------
|
||||
// |
|
||||
// Copyright 2015-2023 Łukasz "JustArchi" Domeradzki
|
||||
// Copyright 2015-2024 Łukasz "JustArchi" Domeradzki
|
||||
// Contact: JustArchi@JustArchi.net
|
||||
// |
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
// ----------------------------------------------------------------------------------------------
|
||||
// _ _ _ ____ _ _____
|
||||
// / \ _ __ ___ | |__ (_)/ ___| | |_ ___ __ _ _ __ ___ | ___|__ _ _ __ _ __ ___
|
||||
// / _ \ | '__|/ __|| '_ \ | |\___ \ | __|/ _ \ / _` || '_ ` _ \ | |_ / _` || '__|| '_ ` _ \
|
||||
// / ___ \ | | | (__ | | | || | ___) || |_| __/| (_| || | | | | || _|| (_| || | | | | | | |
|
||||
// /_/ \_\|_| \___||_| |_||_||____/ \__|\___| \__,_||_| |_| |_||_| \__,_||_| |_| |_| |_|
|
||||
// ----------------------------------------------------------------------------------------------
|
||||
// |
|
||||
// Copyright 2015-2023 Łukasz "JustArchi" Domeradzki
|
||||
// Copyright 2015-2024 Łukasz "JustArchi" Domeradzki
|
||||
// Contact: JustArchi@JustArchi.net
|
||||
// |
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
@@ -24,11 +26,12 @@ using System.Collections;
|
||||
using System.Collections.Concurrent;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text.Json.Serialization;
|
||||
using JetBrains.Annotations;
|
||||
|
||||
namespace ArchiSteamFarm.Collections;
|
||||
|
||||
public sealed class ConcurrentHashSet<T> : IReadOnlyCollection<T>, ISet<T> where T : notnull {
|
||||
public sealed class ConcurrentHashSet<T> : IReadOnlySet<T>, ISet<T> where T : notnull {
|
||||
[PublicAPI]
|
||||
public event EventHandler? OnModified;
|
||||
|
||||
@@ -37,15 +40,31 @@ public sealed class ConcurrentHashSet<T> : IReadOnlyCollection<T>, ISet<T> where
|
||||
|
||||
private readonly ConcurrentDictionary<T, bool> BackingCollection;
|
||||
|
||||
[JsonConstructor]
|
||||
public ConcurrentHashSet() => BackingCollection = new ConcurrentDictionary<T, bool>();
|
||||
|
||||
public ConcurrentHashSet(IEnumerable<T> collection) {
|
||||
ArgumentNullException.ThrowIfNull(collection);
|
||||
|
||||
BackingCollection = new ConcurrentDictionary<T, bool>(collection.Select(static item => new KeyValuePair<T, bool>(item, true)));
|
||||
}
|
||||
|
||||
public ConcurrentHashSet(IEqualityComparer<T> comparer) {
|
||||
ArgumentNullException.ThrowIfNull(comparer);
|
||||
|
||||
BackingCollection = new ConcurrentDictionary<T, bool>(comparer);
|
||||
}
|
||||
|
||||
public ConcurrentHashSet(IEnumerable<T> collection, IEqualityComparer<T> comparer) {
|
||||
ArgumentNullException.ThrowIfNull(collection);
|
||||
ArgumentNullException.ThrowIfNull(comparer);
|
||||
|
||||
BackingCollection = new ConcurrentDictionary<T, bool>(collection.Select(static item => new KeyValuePair<T, bool>(item, true)), comparer);
|
||||
}
|
||||
|
||||
public bool Add(T item) {
|
||||
ArgumentNullException.ThrowIfNull(item);
|
||||
|
||||
if (!BackingCollection.TryAdd(item, true)) {
|
||||
return false;
|
||||
}
|
||||
@@ -65,9 +84,18 @@ public sealed class ConcurrentHashSet<T> : IReadOnlyCollection<T>, ISet<T> where
|
||||
OnModified?.Invoke(this, EventArgs.Empty);
|
||||
}
|
||||
|
||||
public bool Contains(T item) => BackingCollection.ContainsKey(item);
|
||||
public bool Contains(T item) {
|
||||
ArgumentNullException.ThrowIfNull(item);
|
||||
|
||||
public void CopyTo(T[] array, int arrayIndex) => BackingCollection.Keys.CopyTo(array, arrayIndex);
|
||||
return BackingCollection.ContainsKey(item);
|
||||
}
|
||||
|
||||
public void CopyTo(T[] array, int arrayIndex) {
|
||||
ArgumentNullException.ThrowIfNull(array);
|
||||
ArgumentOutOfRangeException.ThrowIfNegative(arrayIndex);
|
||||
|
||||
BackingCollection.Keys.CopyTo(array, arrayIndex);
|
||||
}
|
||||
|
||||
public void ExceptWith(IEnumerable<T> other) {
|
||||
ArgumentNullException.ThrowIfNull(other);
|
||||
@@ -80,6 +108,8 @@ public sealed class ConcurrentHashSet<T> : IReadOnlyCollection<T>, ISet<T> where
|
||||
public IEnumerator<T> GetEnumerator() => BackingCollection.Keys.GetEnumerator();
|
||||
|
||||
public void IntersectWith(IEnumerable<T> other) {
|
||||
ArgumentNullException.ThrowIfNull(other);
|
||||
|
||||
ISet<T> otherSet = other as ISet<T> ?? other.ToHashSet();
|
||||
|
||||
foreach (T item in this.Where(item => !otherSet.Contains(item))) {
|
||||
@@ -88,36 +118,48 @@ public sealed class ConcurrentHashSet<T> : IReadOnlyCollection<T>, ISet<T> where
|
||||
}
|
||||
|
||||
public bool IsProperSubsetOf(IEnumerable<T> other) {
|
||||
ArgumentNullException.ThrowIfNull(other);
|
||||
|
||||
ISet<T> otherSet = other as ISet<T> ?? other.ToHashSet();
|
||||
|
||||
return (otherSet.Count > Count) && IsSubsetOf(otherSet);
|
||||
}
|
||||
|
||||
public bool IsProperSupersetOf(IEnumerable<T> other) {
|
||||
ArgumentNullException.ThrowIfNull(other);
|
||||
|
||||
ISet<T> otherSet = other as ISet<T> ?? other.ToHashSet();
|
||||
|
||||
return (otherSet.Count < Count) && IsSupersetOf(otherSet);
|
||||
}
|
||||
|
||||
public bool IsSubsetOf(IEnumerable<T> other) {
|
||||
ArgumentNullException.ThrowIfNull(other);
|
||||
|
||||
ISet<T> otherSet = other as ISet<T> ?? other.ToHashSet();
|
||||
|
||||
return this.All(otherSet.Contains);
|
||||
}
|
||||
|
||||
public bool IsSupersetOf(IEnumerable<T> other) {
|
||||
ArgumentNullException.ThrowIfNull(other);
|
||||
|
||||
ISet<T> otherSet = other as ISet<T> ?? other.ToHashSet();
|
||||
|
||||
return otherSet.All(Contains);
|
||||
}
|
||||
|
||||
public bool Overlaps(IEnumerable<T> other) {
|
||||
ArgumentNullException.ThrowIfNull(other);
|
||||
|
||||
ISet<T> otherSet = other as ISet<T> ?? other.ToHashSet();
|
||||
|
||||
return otherSet.Any(Contains);
|
||||
}
|
||||
|
||||
public bool Remove(T item) {
|
||||
ArgumentNullException.ThrowIfNull(item);
|
||||
|
||||
if (!BackingCollection.TryRemove(item, out _)) {
|
||||
return false;
|
||||
}
|
||||
@@ -128,14 +170,18 @@ public sealed class ConcurrentHashSet<T> : IReadOnlyCollection<T>, ISet<T> where
|
||||
}
|
||||
|
||||
public bool SetEquals(IEnumerable<T> other) {
|
||||
ArgumentNullException.ThrowIfNull(other);
|
||||
|
||||
ISet<T> otherSet = other as ISet<T> ?? other.ToHashSet();
|
||||
|
||||
return (otherSet.Count == Count) && otherSet.All(Contains);
|
||||
}
|
||||
|
||||
public void SymmetricExceptWith(IEnumerable<T> other) {
|
||||
ArgumentNullException.ThrowIfNull(other);
|
||||
|
||||
ISet<T> otherSet = other as ISet<T> ?? other.ToHashSet();
|
||||
HashSet<T> removed = new();
|
||||
HashSet<T> removed = [];
|
||||
|
||||
foreach (T item in otherSet.Where(Contains)) {
|
||||
removed.Add(item);
|
||||
@@ -155,12 +201,18 @@ public sealed class ConcurrentHashSet<T> : IReadOnlyCollection<T>, ISet<T> where
|
||||
}
|
||||
}
|
||||
|
||||
void ICollection<T>.Add(T item) => Add(item);
|
||||
void ICollection<T>.Add(T item) {
|
||||
ArgumentNullException.ThrowIfNull(item);
|
||||
|
||||
Add(item);
|
||||
}
|
||||
|
||||
IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
|
||||
|
||||
[PublicAPI]
|
||||
public bool AddRange(IEnumerable<T> items) {
|
||||
ArgumentNullException.ThrowIfNull(items);
|
||||
|
||||
bool result = false;
|
||||
|
||||
foreach (T _ in items.Where(Add)) {
|
||||
@@ -172,6 +224,8 @@ public sealed class ConcurrentHashSet<T> : IReadOnlyCollection<T>, ISet<T> where
|
||||
|
||||
[PublicAPI]
|
||||
public bool RemoveRange(IEnumerable<T> items) {
|
||||
ArgumentNullException.ThrowIfNull(items);
|
||||
|
||||
bool result = false;
|
||||
|
||||
foreach (T _ in items.Where(Remove)) {
|
||||
@@ -181,8 +235,17 @@ public sealed class ConcurrentHashSet<T> : IReadOnlyCollection<T>, ISet<T> where
|
||||
return result;
|
||||
}
|
||||
|
||||
[PublicAPI]
|
||||
public int RemoveWhere(Predicate<T> match) {
|
||||
ArgumentNullException.ThrowIfNull(match);
|
||||
|
||||
return BackingCollection.Keys.Where(match.Invoke).Count(key => BackingCollection.TryRemove(key, out _));
|
||||
}
|
||||
|
||||
[PublicAPI]
|
||||
public bool ReplaceIfNeededWith(IReadOnlyCollection<T> other) {
|
||||
ArgumentNullException.ThrowIfNull(other);
|
||||
|
||||
if (SetEquals(other)) {
|
||||
return false;
|
||||
}
|
||||
@@ -194,6 +257,8 @@ public sealed class ConcurrentHashSet<T> : IReadOnlyCollection<T>, ISet<T> where
|
||||
|
||||
[PublicAPI]
|
||||
public void ReplaceWith(IEnumerable<T> other) {
|
||||
ArgumentNullException.ThrowIfNull(other);
|
||||
|
||||
Clear();
|
||||
UnionWith(other);
|
||||
}
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
// ----------------------------------------------------------------------------------------------
|
||||
// _ _ _ ____ _ _____
|
||||
// / \ _ __ ___ | |__ (_)/ ___| | |_ ___ __ _ _ __ ___ | ___|__ _ _ __ _ __ ___
|
||||
// / _ \ | '__|/ __|| '_ \ | |\___ \ | __|/ _ \ / _` || '_ ` _ \ | |_ / _` || '__|| '_ ` _ \
|
||||
// / ___ \ | | | (__ | | | || | ___) || |_| __/| (_| || | | | | || _|| (_| || | | | | | | |
|
||||
// /_/ \_\|_| \___||_| |_||_||____/ \__|\___| \__,_||_| |_| |_||_| \__,_||_| |_| |_| |_|
|
||||
// ----------------------------------------------------------------------------------------------
|
||||
// |
|
||||
// Copyright 2015-2023 Łukasz "JustArchi" Domeradzki
|
||||
// Copyright 2015-2024 Łukasz "JustArchi" Domeradzki
|
||||
// Contact: JustArchi@JustArchi.net
|
||||
// |
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
@@ -22,18 +24,18 @@
|
||||
using System;
|
||||
using System.Collections;
|
||||
using System.Collections.Generic;
|
||||
using System.Text.Json.Serialization;
|
||||
using JetBrains.Annotations;
|
||||
using Nito.AsyncEx;
|
||||
|
||||
namespace ArchiSteamFarm.Collections;
|
||||
|
||||
internal sealed class ConcurrentList<T> : IList<T>, IReadOnlyList<T> {
|
||||
public sealed class ConcurrentList<T> : IList<T>, IReadOnlyList<T> where T : notnull {
|
||||
[PublicAPI]
|
||||
public event EventHandler? OnModified;
|
||||
|
||||
public bool IsReadOnly => false;
|
||||
|
||||
internal int Count {
|
||||
[PublicAPI]
|
||||
public int Count {
|
||||
get {
|
||||
using (Lock.ReaderLock()) {
|
||||
return BackingCollection.Count;
|
||||
@@ -41,7 +43,9 @@ internal sealed class ConcurrentList<T> : IList<T>, IReadOnlyList<T> {
|
||||
}
|
||||
}
|
||||
|
||||
private readonly List<T> BackingCollection = new();
|
||||
public bool IsReadOnly => false;
|
||||
|
||||
private readonly List<T> BackingCollection;
|
||||
private readonly AsyncReaderWriterLock Lock = new();
|
||||
|
||||
int ICollection<T>.Count => Count;
|
||||
@@ -55,6 +59,8 @@ internal sealed class ConcurrentList<T> : IList<T>, IReadOnlyList<T> {
|
||||
}
|
||||
|
||||
set {
|
||||
ArgumentNullException.ThrowIfNull(value);
|
||||
|
||||
using (Lock.WriterLock()) {
|
||||
BackingCollection[index] = value;
|
||||
}
|
||||
@@ -63,7 +69,18 @@ internal sealed class ConcurrentList<T> : IList<T>, IReadOnlyList<T> {
|
||||
}
|
||||
}
|
||||
|
||||
[JsonConstructor]
|
||||
public ConcurrentList() => BackingCollection = [];
|
||||
|
||||
public ConcurrentList(IEnumerable<T> collection) {
|
||||
ArgumentNullException.ThrowIfNull(collection);
|
||||
|
||||
BackingCollection = [..collection];
|
||||
}
|
||||
|
||||
public void Add(T item) {
|
||||
ArgumentNullException.ThrowIfNull(item);
|
||||
|
||||
using (Lock.WriterLock()) {
|
||||
BackingCollection.Add(item);
|
||||
}
|
||||
@@ -80,12 +97,17 @@ internal sealed class ConcurrentList<T> : IList<T>, IReadOnlyList<T> {
|
||||
}
|
||||
|
||||
public bool Contains(T item) {
|
||||
ArgumentNullException.ThrowIfNull(item);
|
||||
|
||||
using (Lock.ReaderLock()) {
|
||||
return BackingCollection.Contains(item);
|
||||
}
|
||||
}
|
||||
|
||||
public void CopyTo(T[] array, int arrayIndex) {
|
||||
ArgumentNullException.ThrowIfNull(array);
|
||||
ArgumentOutOfRangeException.ThrowIfNegative(arrayIndex);
|
||||
|
||||
using (Lock.ReaderLock()) {
|
||||
BackingCollection.CopyTo(array, arrayIndex);
|
||||
}
|
||||
@@ -94,12 +116,17 @@ internal sealed class ConcurrentList<T> : IList<T>, IReadOnlyList<T> {
|
||||
public IEnumerator<T> GetEnumerator() => new ConcurrentEnumerator<T>(BackingCollection, Lock.ReaderLock());
|
||||
|
||||
public int IndexOf(T item) {
|
||||
ArgumentNullException.ThrowIfNull(item);
|
||||
|
||||
using (Lock.ReaderLock()) {
|
||||
return BackingCollection.IndexOf(item);
|
||||
}
|
||||
}
|
||||
|
||||
public void Insert(int index, T item) {
|
||||
ArgumentOutOfRangeException.ThrowIfNegative(index);
|
||||
ArgumentNullException.ThrowIfNull(item);
|
||||
|
||||
using (Lock.WriterLock()) {
|
||||
BackingCollection.Insert(index, item);
|
||||
}
|
||||
@@ -108,6 +135,8 @@ internal sealed class ConcurrentList<T> : IList<T>, IReadOnlyList<T> {
|
||||
}
|
||||
|
||||
public bool Remove(T item) {
|
||||
ArgumentNullException.ThrowIfNull(item);
|
||||
|
||||
using (Lock.WriterLock()) {
|
||||
if (!BackingCollection.Remove(item)) {
|
||||
return false;
|
||||
@@ -120,6 +149,8 @@ internal sealed class ConcurrentList<T> : IList<T>, IReadOnlyList<T> {
|
||||
}
|
||||
|
||||
public void RemoveAt(int index) {
|
||||
ArgumentOutOfRangeException.ThrowIfNegative(index);
|
||||
|
||||
using (Lock.WriterLock()) {
|
||||
BackingCollection.RemoveAt(index);
|
||||
}
|
||||
@@ -129,7 +160,10 @@ internal sealed class ConcurrentList<T> : IList<T>, IReadOnlyList<T> {
|
||||
|
||||
IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
|
||||
|
||||
internal void ReplaceWith(IEnumerable<T> collection) {
|
||||
[PublicAPI]
|
||||
public void ReplaceWith(IEnumerable<T> collection) {
|
||||
ArgumentNullException.ThrowIfNull(collection);
|
||||
|
||||
using (Lock.WriterLock()) {
|
||||
BackingCollection.Clear();
|
||||
BackingCollection.AddRange(collection);
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
// ----------------------------------------------------------------------------------------------
|
||||
// _ _ _ ____ _ _____
|
||||
// / \ _ __ ___ | |__ (_)/ ___| | |_ ___ __ _ _ __ ___ | ___|__ _ _ __ _ __ ___
|
||||
// / _ \ | '__|/ __|| '_ \ | |\___ \ | __|/ _ \ / _` || '_ ` _ \ | |_ / _` || '__|| '_ ` _ \
|
||||
// / ___ \ | | | (__ | | | || | ___) || |_| __/| (_| || | | | | || _|| (_| || | | | | | | |
|
||||
// /_/ \_\|_| \___||_| |_||_||____/ \__|\___| \__,_||_| |_| |_||_| \__,_||_| |_| |_| |_|
|
||||
// ----------------------------------------------------------------------------------------------
|
||||
// |
|
||||
// Copyright 2015-2023 Łukasz "JustArchi" Domeradzki
|
||||
// Copyright 2015-2024 Łukasz "JustArchi" Domeradzki
|
||||
// Contact: JustArchi@JustArchi.net
|
||||
// |
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
@@ -23,22 +25,17 @@ using System;
|
||||
using System.Collections;
|
||||
using System.Collections.Concurrent;
|
||||
using System.Collections.Generic;
|
||||
using ArchiSteamFarm.Core;
|
||||
|
||||
namespace ArchiSteamFarm.Collections;
|
||||
|
||||
internal sealed class FixedSizeConcurrentQueue<T> : IEnumerable<T> {
|
||||
internal sealed class FixedSizeConcurrentQueue<T> : IEnumerable<T> where T : notnull {
|
||||
private readonly ConcurrentQueue<T> BackingQueue = new();
|
||||
|
||||
internal byte MaxCount {
|
||||
get => BackingMaxCount;
|
||||
|
||||
set {
|
||||
if (value == 0) {
|
||||
ASF.ArchiLogger.LogNullError(value);
|
||||
|
||||
return;
|
||||
}
|
||||
ArgumentOutOfRangeException.ThrowIfZero(value);
|
||||
|
||||
BackingMaxCount = value;
|
||||
|
||||
@@ -58,6 +55,8 @@ internal sealed class FixedSizeConcurrentQueue<T> : IEnumerable<T> {
|
||||
IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
|
||||
|
||||
internal void Enqueue(T obj) {
|
||||
ArgumentNullException.ThrowIfNull(obj);
|
||||
|
||||
BackingQueue.Enqueue(obj);
|
||||
|
||||
Resize();
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
// ----------------------------------------------------------------------------------------------
|
||||
// _ _ _ ____ _ _____
|
||||
// / \ _ __ ___ | |__ (_)/ ___| | |_ ___ __ _ _ __ ___ | ___|__ _ _ __ _ __ ___
|
||||
// / _ \ | '__|/ __|| '_ \ | |\___ \ | __|/ _ \ / _` || '_ ` _ \ | |_ / _` || '__|| '_ ` _ \
|
||||
// / ___ \ | | | (__ | | | || | ___) || |_| __/| (_| || | | | | || _|| (_| || | | | | | | |
|
||||
// /_/ \_\|_| \___||_| |_||_||____/ \__|\___| \__,_||_| |_| |_||_| \__,_||_| |_| |_| |_|
|
||||
// ----------------------------------------------------------------------------------------------
|
||||
// |
|
||||
// Copyright 2015-2023 Łukasz "JustArchi" Domeradzki
|
||||
// Copyright 2015-2024 Łukasz "JustArchi" Domeradzki
|
||||
// Contact: JustArchi@JustArchi.net
|
||||
// |
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
@@ -23,12 +25,13 @@ using System;
|
||||
using System.Collections;
|
||||
using System.Collections.Concurrent;
|
||||
using System.Collections.Generic;
|
||||
using System.Text.Json.Serialization;
|
||||
using JetBrains.Annotations;
|
||||
using Newtonsoft.Json;
|
||||
|
||||
namespace ArchiSteamFarm.Collections;
|
||||
|
||||
public sealed class ObservableConcurrentDictionary<TKey, TValue> : IDictionary<TKey, TValue>, IReadOnlyDictionary<TKey, TValue> where TKey : notnull {
|
||||
[PublicAPI]
|
||||
public event EventHandler? OnModified;
|
||||
|
||||
[PublicAPI]
|
||||
@@ -42,8 +45,7 @@ public sealed class ObservableConcurrentDictionary<TKey, TValue> : IDictionary<T
|
||||
[PublicAPI]
|
||||
public ICollection<TKey> Keys => BackingDictionary.Keys;
|
||||
|
||||
[JsonProperty(Required = Required.DisallowNull)]
|
||||
private readonly ConcurrentDictionary<TKey, TValue> BackingDictionary = new();
|
||||
private readonly ConcurrentDictionary<TKey, TValue> BackingDictionary;
|
||||
|
||||
int ICollection<KeyValuePair<TKey, TValue>>.Count => BackingDictionary.Count;
|
||||
int IReadOnlyCollection<KeyValuePair<TKey, TValue>>.Count => BackingDictionary.Count;
|
||||
@@ -54,7 +56,10 @@ public sealed class ObservableConcurrentDictionary<TKey, TValue> : IDictionary<T
|
||||
|
||||
public TValue this[TKey key] {
|
||||
get => BackingDictionary[key];
|
||||
|
||||
set {
|
||||
ArgumentNullException.ThrowIfNull(value);
|
||||
|
||||
if (BackingDictionary.TryGetValue(key, out TValue? savedValue) && EqualityComparer<TValue>.Default.Equals(savedValue, value)) {
|
||||
return;
|
||||
}
|
||||
@@ -64,13 +69,41 @@ public sealed class ObservableConcurrentDictionary<TKey, TValue> : IDictionary<T
|
||||
}
|
||||
}
|
||||
|
||||
[JsonConstructor]
|
||||
public ObservableConcurrentDictionary() => BackingDictionary = new ConcurrentDictionary<TKey, TValue>();
|
||||
|
||||
public ObservableConcurrentDictionary(IEnumerable<KeyValuePair<TKey, TValue>> collection) {
|
||||
ArgumentNullException.ThrowIfNull(collection);
|
||||
|
||||
BackingDictionary = new ConcurrentDictionary<TKey, TValue>(collection);
|
||||
}
|
||||
|
||||
public ObservableConcurrentDictionary(IEqualityComparer<TKey> comparer) {
|
||||
ArgumentNullException.ThrowIfNull(comparer);
|
||||
|
||||
BackingDictionary = new ConcurrentDictionary<TKey, TValue>(comparer);
|
||||
}
|
||||
|
||||
public ObservableConcurrentDictionary(IEnumerable<KeyValuePair<TKey, TValue>> collection, IEqualityComparer<TKey> comparer) {
|
||||
ArgumentNullException.ThrowIfNull(collection);
|
||||
ArgumentNullException.ThrowIfNull(comparer);
|
||||
|
||||
BackingDictionary = new ConcurrentDictionary<TKey, TValue>(collection, comparer);
|
||||
}
|
||||
|
||||
public void Add(KeyValuePair<TKey, TValue> item) {
|
||||
(TKey key, TValue value) = item;
|
||||
|
||||
Add(key, value);
|
||||
}
|
||||
|
||||
public void Add(TKey key, TValue value) => TryAdd(key, value);
|
||||
public void Add(TKey key, TValue value) {
|
||||
ArgumentNullException.ThrowIfNull(key);
|
||||
|
||||
if (!TryAdd(key, value)) {
|
||||
throw new ArgumentException($"An item with the same key has already been added. Key: {key}");
|
||||
}
|
||||
}
|
||||
|
||||
public void Clear() {
|
||||
if (BackingDictionary.IsEmpty) {
|
||||
@@ -82,7 +115,14 @@ public sealed class ObservableConcurrentDictionary<TKey, TValue> : IDictionary<T
|
||||
}
|
||||
|
||||
public bool Contains(KeyValuePair<TKey, TValue> item) => ((ICollection<KeyValuePair<TKey, TValue>>) BackingDictionary).Contains(item);
|
||||
public void CopyTo(KeyValuePair<TKey, TValue>[] array, int arrayIndex) => ((ICollection<KeyValuePair<TKey, TValue>>) BackingDictionary).CopyTo(array, arrayIndex);
|
||||
|
||||
public void CopyTo(KeyValuePair<TKey, TValue>[] array, int arrayIndex) {
|
||||
ArgumentNullException.ThrowIfNull(array);
|
||||
ArgumentOutOfRangeException.ThrowIfNegative(arrayIndex);
|
||||
|
||||
((ICollection<KeyValuePair<TKey, TValue>>) BackingDictionary).CopyTo(array, arrayIndex);
|
||||
}
|
||||
|
||||
public IEnumerator<KeyValuePair<TKey, TValue>> GetEnumerator() => BackingDictionary.GetEnumerator();
|
||||
|
||||
public bool Remove(KeyValuePair<TKey, TValue> item) {
|
||||
@@ -98,6 +138,8 @@ public sealed class ObservableConcurrentDictionary<TKey, TValue> : IDictionary<T
|
||||
}
|
||||
|
||||
public bool Remove(TKey key) {
|
||||
ArgumentNullException.ThrowIfNull(key);
|
||||
|
||||
if (!BackingDictionary.TryRemove(key, out _)) {
|
||||
return false;
|
||||
}
|
||||
@@ -107,14 +149,36 @@ public sealed class ObservableConcurrentDictionary<TKey, TValue> : IDictionary<T
|
||||
return true;
|
||||
}
|
||||
|
||||
bool IDictionary<TKey, TValue>.ContainsKey(TKey key) => BackingDictionary.ContainsKey(key);
|
||||
bool IReadOnlyDictionary<TKey, TValue>.ContainsKey(TKey key) => BackingDictionary.ContainsKey(key);
|
||||
bool IDictionary<TKey, TValue>.ContainsKey(TKey key) {
|
||||
ArgumentNullException.ThrowIfNull(key);
|
||||
|
||||
return BackingDictionary.ContainsKey(key);
|
||||
}
|
||||
|
||||
bool IReadOnlyDictionary<TKey, TValue>.ContainsKey(TKey key) {
|
||||
ArgumentNullException.ThrowIfNull(key);
|
||||
|
||||
return BackingDictionary.ContainsKey(key);
|
||||
}
|
||||
|
||||
IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
|
||||
bool IReadOnlyDictionary<TKey, TValue>.TryGetValue(TKey key, out TValue value) => BackingDictionary.TryGetValue(key, out value!);
|
||||
bool IDictionary<TKey, TValue>.TryGetValue(TKey key, out TValue value) => BackingDictionary.TryGetValue(key, out value!);
|
||||
|
||||
bool IReadOnlyDictionary<TKey, TValue>.TryGetValue(TKey key, out TValue value) {
|
||||
ArgumentNullException.ThrowIfNull(key);
|
||||
|
||||
return BackingDictionary.TryGetValue(key, out value!);
|
||||
}
|
||||
|
||||
bool IDictionary<TKey, TValue>.TryGetValue(TKey key, out TValue value) {
|
||||
ArgumentNullException.ThrowIfNull(key);
|
||||
|
||||
return BackingDictionary.TryGetValue(key, out value!);
|
||||
}
|
||||
|
||||
[PublicAPI]
|
||||
public bool TryAdd(TKey key, TValue value) {
|
||||
ArgumentNullException.ThrowIfNull(key);
|
||||
|
||||
if (!BackingDictionary.TryAdd(key, value)) {
|
||||
return false;
|
||||
}
|
||||
@@ -125,5 +189,9 @@ public sealed class ObservableConcurrentDictionary<TKey, TValue> : IDictionary<T
|
||||
}
|
||||
|
||||
[PublicAPI]
|
||||
public bool TryGetValue(TKey key, out TValue? value) => BackingDictionary.TryGetValue(key, out value);
|
||||
public bool TryGetValue(TKey key, out TValue? value) {
|
||||
ArgumentNullException.ThrowIfNull(key);
|
||||
|
||||
return BackingDictionary.TryGetValue(key, out value);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
// ----------------------------------------------------------------------------------------------
|
||||
// _ _ _ ____ _ _____
|
||||
// / \ _ __ ___ | |__ (_)/ ___| | |_ ___ __ _ _ __ ___ | ___|__ _ _ __ _ __ ___
|
||||
// / _ \ | '__|/ __|| '_ \ | |\___ \ | __|/ _ \ / _` || '_ ` _ \ | |_ / _` || '__|| '_ ` _ \
|
||||
// / ___ \ | | | (__ | | | || | ___) || |_| __/| (_| || | | | | || _|| (_| || | | | | | | |
|
||||
// /_/ \_\|_| \___||_| |_||_||____/ \__|\___| \__,_||_| |_| |_||_| \__,_||_| |_| |_| |_|
|
||||
// ----------------------------------------------------------------------------------------------
|
||||
// |
|
||||
// Copyright 2015-2023 Łukasz "JustArchi" Domeradzki
|
||||
// Copyright 2015-2024 Łukasz "JustArchi" Domeradzki
|
||||
// Contact: JustArchi@JustArchi.net
|
||||
// |
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
@@ -21,8 +23,8 @@
|
||||
|
||||
using System;
|
||||
using System.Collections.Concurrent;
|
||||
using System.Collections.Frozen;
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.Immutable;
|
||||
using System.ComponentModel;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.Globalization;
|
||||
@@ -41,6 +43,8 @@ using ArchiSteamFarm.Steam;
|
||||
using ArchiSteamFarm.Steam.Integration;
|
||||
using ArchiSteamFarm.Storage;
|
||||
using ArchiSteamFarm.Web;
|
||||
using ArchiSteamFarm.Web.GitHub;
|
||||
using ArchiSteamFarm.Web.GitHub.Data;
|
||||
using ArchiSteamFarm.Web.Responses;
|
||||
using JetBrains.Annotations;
|
||||
using SteamKit2;
|
||||
@@ -75,9 +79,9 @@ public static class ASF {
|
||||
internal static ICrossProcessSemaphore? LoginRateLimitingSemaphore { get; private set; }
|
||||
internal static ICrossProcessSemaphore? LoginSemaphore { get; private set; }
|
||||
internal static ICrossProcessSemaphore? RateLimitingSemaphore { get; private set; }
|
||||
internal static ImmutableDictionary<Uri, (ICrossProcessSemaphore RateLimitingSemaphore, SemaphoreSlim OpenConnectionsSemaphore)>? WebLimitingSemaphores { get; private set; }
|
||||
internal static FrozenDictionary<Uri, (ICrossProcessSemaphore RateLimitingSemaphore, SemaphoreSlim OpenConnectionsSemaphore)>? WebLimitingSemaphores { get; private set; }
|
||||
|
||||
private static readonly ImmutableHashSet<string> AssembliesNeededBeforeUpdate = ImmutableHashSet.Create(StringComparer.Ordinal, "System.IO.Pipes");
|
||||
private static readonly FrozenSet<string> AssembliesNeededBeforeUpdate = new HashSet<string>(1, StringComparer.Ordinal) { "System.IO.Pipes" }.ToFrozenSet(StringComparer.Ordinal);
|
||||
private static readonly SemaphoreSlim UpdateSemaphore = new(1, 1);
|
||||
|
||||
private static Timer? AutoUpdatesTimer;
|
||||
@@ -101,23 +105,32 @@ public static class ASF {
|
||||
return fileType switch {
|
||||
EFileType.Config => Path.Combine(SharedInfo.ConfigDirectory, SharedInfo.GlobalConfigFileName),
|
||||
EFileType.Database => Path.Combine(SharedInfo.ConfigDirectory, SharedInfo.GlobalDatabaseFileName),
|
||||
EFileType.Crash => Path.Combine(SharedInfo.ConfigDirectory, SharedInfo.GlobalCrashFileName),
|
||||
_ => throw new InvalidOperationException(nameof(fileType))
|
||||
};
|
||||
}
|
||||
|
||||
internal static async Task Init() {
|
||||
internal static async Task<bool> Init() {
|
||||
if (GlobalConfig == null) {
|
||||
throw new InvalidOperationException(nameof(GlobalConfig));
|
||||
}
|
||||
|
||||
if (!PluginsCore.InitPlugins()) {
|
||||
await Task.Delay(SharedInfo.InformationDelay).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
WebBrowser = new WebBrowser(ArchiLogger, GlobalConfig.WebProxy, true);
|
||||
|
||||
if (!await PluginsCore.InitPlugins().ConfigureAwait(false)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
await UpdateAndRestart().ConfigureAwait(false);
|
||||
|
||||
if (!Program.IgnoreUnsupportedEnvironment && !await ProtectAgainstCrashes().ConfigureAwait(false)) {
|
||||
ArchiLogger.LogFatalError(Strings.ErrorTooManyCrashes);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
Program.AllowCrashFileRemoval = true;
|
||||
|
||||
await PluginsCore.OnASFInitModules(GlobalConfig.AdditionalProperties).ConfigureAwait(false);
|
||||
await InitRateLimiters().ConfigureAwait(false);
|
||||
|
||||
@@ -142,6 +155,8 @@ public static class ASF {
|
||||
if (Program.ConfigWatch) {
|
||||
InitConfigWatchEvents();
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
internal static bool IsValidBotName(string botName) {
|
||||
@@ -174,212 +189,23 @@ public static class ASF {
|
||||
}
|
||||
}
|
||||
|
||||
internal static async Task<Version?> Update(GlobalConfig.EUpdateChannel? channel = null, bool updateOverride = false) {
|
||||
if (channel.HasValue && !Enum.IsDefined(channel.Value)) {
|
||||
throw new InvalidEnumArgumentException(nameof(channel), (int) channel, typeof(GlobalConfig.EUpdateChannel));
|
||||
internal static async Task<(bool Updated, Version? NewVersion)> Update(GlobalConfig.EUpdateChannel? updateChannel = null, bool updateOverride = false, bool forced = false) {
|
||||
if (updateChannel.HasValue && !Enum.IsDefined(updateChannel.Value)) {
|
||||
throw new InvalidEnumArgumentException(nameof(updateChannel), (int) updateChannel, typeof(GlobalConfig.EUpdateChannel));
|
||||
}
|
||||
|
||||
if (GlobalConfig == null) {
|
||||
throw new InvalidOperationException(nameof(GlobalConfig));
|
||||
}
|
||||
|
||||
if (WebBrowser == null) {
|
||||
throw new InvalidOperationException(nameof(WebBrowser));
|
||||
(bool updated, Version? newVersion) = await UpdateASF(updateChannel, updateOverride, forced).ConfigureAwait(false);
|
||||
|
||||
if (!updated) {
|
||||
// ASF wasn't updated as part of the process, update the plugins alone
|
||||
updated = await PluginsCore.UpdatePlugins(SharedInfo.Version, false, updateChannel, updateOverride, forced).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
channel ??= GlobalConfig.UpdateChannel;
|
||||
|
||||
if (!SharedInfo.BuildInfo.CanUpdate || (channel == GlobalConfig.EUpdateChannel.None)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
await UpdateSemaphore.WaitAsync().ConfigureAwait(false);
|
||||
|
||||
try {
|
||||
// If backup directory from previous update exists, it's a good idea to purge it now
|
||||
string backupDirectory = Path.Combine(SharedInfo.HomeDirectory, SharedInfo.UpdateDirectory);
|
||||
|
||||
if (Directory.Exists(backupDirectory)) {
|
||||
ArchiLogger.LogGenericInfo(Strings.UpdateCleanup);
|
||||
|
||||
for (byte i = 0; (i < WebBrowser.MaxTries) && Directory.Exists(backupDirectory); i++) {
|
||||
if (i > 0) {
|
||||
// It's entirely possible that old process is still running, wait a short moment for eventual cleanup
|
||||
await Task.Delay(5000).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
try {
|
||||
Directory.Delete(backupDirectory, true);
|
||||
} catch (Exception e) {
|
||||
ArchiLogger.LogGenericDebuggingException(e);
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
if (Directory.Exists(backupDirectory)) {
|
||||
ArchiLogger.LogGenericError(Strings.WarningFailed);
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
ArchiLogger.LogGenericInfo(Strings.Done);
|
||||
}
|
||||
|
||||
ArchiLogger.LogGenericInfo(Strings.UpdateCheckingNewVersion);
|
||||
|
||||
GitHub.ReleaseResponse? releaseResponse = await GitHub.GetLatestRelease(channel == GlobalConfig.EUpdateChannel.Stable).ConfigureAwait(false);
|
||||
|
||||
if (releaseResponse == null) {
|
||||
ArchiLogger.LogGenericWarning(Strings.ErrorUpdateCheckFailed);
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
if (string.IsNullOrEmpty(releaseResponse.Tag)) {
|
||||
ArchiLogger.LogGenericWarning(Strings.ErrorUpdateCheckFailed);
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
Version newVersion = new(releaseResponse.Tag);
|
||||
|
||||
ArchiLogger.LogGenericInfo(string.Format(CultureInfo.CurrentCulture, Strings.UpdateVersionInfo, SharedInfo.Version, newVersion));
|
||||
|
||||
if (SharedInfo.Version >= newVersion) {
|
||||
return newVersion;
|
||||
}
|
||||
|
||||
if (!updateOverride && (GlobalConfig.UpdatePeriod == 0)) {
|
||||
ArchiLogger.LogGenericInfo(Strings.UpdateNewVersionAvailable);
|
||||
await Task.Delay(SharedInfo.ShortInformationDelay).ConfigureAwait(false);
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
// Auto update logic starts here
|
||||
if (releaseResponse.Assets.IsEmpty) {
|
||||
ArchiLogger.LogGenericWarning(Strings.ErrorUpdateNoAssets);
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
string targetFile = $"{SharedInfo.ASF}-{SharedInfo.BuildInfo.Variant}.zip";
|
||||
GitHub.ReleaseResponse.Asset? binaryAsset = releaseResponse.Assets.FirstOrDefault(asset => !string.IsNullOrEmpty(asset.Name) && asset.Name.Equals(targetFile, StringComparison.OrdinalIgnoreCase));
|
||||
|
||||
if (binaryAsset == null) {
|
||||
ArchiLogger.LogGenericWarning(Strings.ErrorUpdateNoAssetForThisVersion);
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
if (binaryAsset.DownloadURL == null) {
|
||||
ArchiLogger.LogNullError(binaryAsset.DownloadURL);
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
ArchiLogger.LogGenericInfo(Strings.FetchingChecksumFromRemoteServer);
|
||||
|
||||
string? remoteChecksum = await ArchiNet.FetchBuildChecksum(newVersion, SharedInfo.BuildInfo.Variant).ConfigureAwait(false);
|
||||
|
||||
switch (remoteChecksum) {
|
||||
case null:
|
||||
// Timeout or error, refuse to update as a security measure
|
||||
return null;
|
||||
case "":
|
||||
// Unknown checksum, release too new or actual malicious build published, no need to scare the user as it's 99.99% the first
|
||||
ArchiLogger.LogGenericWarning(Strings.ChecksumMissing);
|
||||
|
||||
return SharedInfo.Version;
|
||||
}
|
||||
|
||||
if (!string.IsNullOrEmpty(releaseResponse.ChangelogPlainText)) {
|
||||
ArchiLogger.LogGenericInfo(releaseResponse.ChangelogPlainText);
|
||||
}
|
||||
|
||||
ArchiLogger.LogGenericInfo(string.Format(CultureInfo.CurrentCulture, Strings.UpdateDownloadingNewVersion, newVersion, binaryAsset.Size / 1024 / 1024));
|
||||
|
||||
Progress<byte> progressReporter = new();
|
||||
|
||||
progressReporter.ProgressChanged += OnProgressChanged;
|
||||
|
||||
BinaryResponse? response;
|
||||
|
||||
try {
|
||||
response = await WebBrowser.UrlGetToBinary(binaryAsset.DownloadURL, progressReporter: progressReporter).ConfigureAwait(false);
|
||||
} finally {
|
||||
progressReporter.ProgressChanged -= OnProgressChanged;
|
||||
}
|
||||
|
||||
if (response?.Content == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
ArchiLogger.LogGenericInfo(Strings.VerifyingChecksumWithRemoteServer);
|
||||
|
||||
byte[] responseBytes = response.Content as byte[] ?? response.Content.ToArray();
|
||||
|
||||
string checksum = Utilities.GenerateChecksumFor(responseBytes);
|
||||
|
||||
if (!checksum.Equals(remoteChecksum, StringComparison.OrdinalIgnoreCase)) {
|
||||
ArchiLogger.LogGenericError(Strings.ChecksumWrong);
|
||||
|
||||
return SharedInfo.Version;
|
||||
}
|
||||
|
||||
await PluginsCore.OnUpdateProceeding(newVersion).ConfigureAwait(false);
|
||||
|
||||
bool kestrelWasRunning = ArchiKestrel.IsRunning;
|
||||
|
||||
if (kestrelWasRunning) {
|
||||
// We disable ArchiKestrel here as the update process moves the core files and might result in IPC crash
|
||||
// TODO: It might fail if the update was triggered from the API, this should be something to improve in the future, by changing the structure into request -> return response -> finish update
|
||||
try {
|
||||
await ArchiKestrel.Stop().ConfigureAwait(false);
|
||||
} catch (Exception e) {
|
||||
ArchiLogger.LogGenericWarningException(e);
|
||||
}
|
||||
}
|
||||
|
||||
ArchiLogger.LogGenericInfo(Strings.PatchingFiles);
|
||||
|
||||
MemoryStream ms = new(responseBytes);
|
||||
|
||||
try {
|
||||
await using (ms.ConfigureAwait(false)) {
|
||||
using ZipArchive zipArchive = new(ms);
|
||||
|
||||
if (!UpdateFromArchive(zipArchive, SharedInfo.HomeDirectory)) {
|
||||
ArchiLogger.LogGenericError(Strings.WarningFailed);
|
||||
}
|
||||
}
|
||||
} catch (Exception e) {
|
||||
ArchiLogger.LogGenericException(e);
|
||||
|
||||
if (kestrelWasRunning) {
|
||||
// We've temporarily disabled ArchiKestrel but the update has failed, let's bring it back up
|
||||
// We can't even be sure if it's possible to bring it back up in this state, but it's worth trying anyway
|
||||
try {
|
||||
await ArchiKestrel.Start().ConfigureAwait(false);
|
||||
} catch (Exception ex) {
|
||||
ArchiLogger.LogGenericWarningException(ex);
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
ArchiLogger.LogGenericInfo(Strings.UpdateFinished);
|
||||
|
||||
await PluginsCore.OnUpdateFinished(newVersion).ConfigureAwait(false);
|
||||
|
||||
return newVersion;
|
||||
} finally {
|
||||
UpdateSemaphore.Release();
|
||||
}
|
||||
return (updated, newVersion);
|
||||
}
|
||||
|
||||
private static async Task<bool> CanHandleWriteEvent(string filePath) {
|
||||
@@ -441,7 +267,7 @@ public static class ASF {
|
||||
{ ArchiWebHandler.SteamHelpURL, (await PluginsCore.GetCrossProcessSemaphore($"{nameof(ArchiWebHandler)}-{nameof(ArchiWebHandler.SteamHelpURL)}").ConfigureAwait(false), new SemaphoreSlim(WebBrowser.MaxConnections, WebBrowser.MaxConnections)) },
|
||||
{ ArchiWebHandler.SteamStoreURL, (await PluginsCore.GetCrossProcessSemaphore($"{nameof(ArchiWebHandler)}-{nameof(ArchiWebHandler.SteamStoreURL)}").ConfigureAwait(false), new SemaphoreSlim(WebBrowser.MaxConnections, WebBrowser.MaxConnections)) },
|
||||
{ WebAPI.DefaultBaseAddress, (await PluginsCore.GetCrossProcessSemaphore($"{nameof(ArchiWebHandler)}-{nameof(WebAPI)}").ConfigureAwait(false), new SemaphoreSlim(WebBrowser.MaxConnections, WebBrowser.MaxConnections)) }
|
||||
}.ToImmutableDictionary();
|
||||
}.ToFrozenDictionary();
|
||||
}
|
||||
|
||||
private static void LoadAllAssemblies() {
|
||||
@@ -789,16 +615,6 @@ public static class ASF {
|
||||
}
|
||||
}
|
||||
|
||||
private static void OnProgressChanged(object? sender, byte progressPercentage) {
|
||||
const byte printEveryPercentage = 10;
|
||||
|
||||
if (progressPercentage % printEveryPercentage != 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
ArchiLogger.LogGenericDebug($"{progressPercentage}%...");
|
||||
}
|
||||
|
||||
private static async void OnRenamed(object sender, RenamedEventArgs e) {
|
||||
// This function can be called with a possibility of OldName or (new) Name being null, we have to take it into account
|
||||
ArgumentNullException.ThrowIfNull(sender);
|
||||
@@ -813,6 +629,37 @@ public static class ASF {
|
||||
}
|
||||
}
|
||||
|
||||
private static async Task<bool> ProtectAgainstCrashes() {
|
||||
if (Debugging.IsDebugBuild) {
|
||||
// Allow debug builds to run unconditionally, we expect to crash a lot in those
|
||||
return true;
|
||||
}
|
||||
|
||||
string crashFilePath = GetFilePath(EFileType.Crash);
|
||||
|
||||
CrashFile crashFile = await CrashFile.CreateOrLoad(crashFilePath).ConfigureAwait(false);
|
||||
|
||||
if (crashFile.StartupCount >= WebBrowser.MaxTries) {
|
||||
// We've reached maximum allowed count of recent crashes, return failure
|
||||
return false;
|
||||
}
|
||||
|
||||
DateTime now = DateTime.UtcNow;
|
||||
|
||||
if (now - crashFile.LastStartup > TimeSpan.FromMinutes(5)) {
|
||||
// Last crash was long ago, restart counter
|
||||
crashFile.StartupCount = 1;
|
||||
} else if (++crashFile.StartupCount >= WebBrowser.MaxTries) {
|
||||
// We've reached maximum allowed count of recent crashes, return failure
|
||||
return false;
|
||||
}
|
||||
|
||||
crashFile.LastStartup = now;
|
||||
|
||||
// We're allowing this run to proceed
|
||||
return true;
|
||||
}
|
||||
|
||||
private static async Task RegisterBots() {
|
||||
if (GlobalConfig == null) {
|
||||
throw new InvalidOperationException(nameof(GlobalConfig));
|
||||
@@ -873,7 +720,7 @@ public static class ASF {
|
||||
throw new InvalidOperationException(nameof(GlobalConfig));
|
||||
}
|
||||
|
||||
if (!SharedInfo.BuildInfo.CanUpdate || (GlobalConfig.UpdateChannel == GlobalConfig.EUpdateChannel.None)) {
|
||||
if (GlobalConfig.UpdateChannel == GlobalConfig.EUpdateChannel.None) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -890,14 +737,11 @@ public static class ASF {
|
||||
ArchiLogger.LogGenericInfo(string.Format(CultureInfo.CurrentCulture, Strings.AutoUpdateCheckInfo, autoUpdatePeriod.ToHumanReadable()));
|
||||
}
|
||||
|
||||
Version? newVersion = await Update().ConfigureAwait(false);
|
||||
(bool updated, Version? newVersion) = await Update().ConfigureAwait(false);
|
||||
|
||||
if (newVersion == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (SharedInfo.Version >= newVersion) {
|
||||
if (SharedInfo.Version > newVersion) {
|
||||
if (!updated) {
|
||||
if ((newVersion != null) && (SharedInfo.Version > newVersion)) {
|
||||
// User is running version newer than their channel allows
|
||||
ArchiLogger.LogGenericWarning(Strings.WarningPreReleaseVersion);
|
||||
await Task.Delay(SharedInfo.InformationDelay).ConfigureAwait(false);
|
||||
}
|
||||
@@ -905,12 +749,230 @@ public static class ASF {
|
||||
return;
|
||||
}
|
||||
|
||||
// Allow crash file recovery, if needed
|
||||
Program.AllowCrashFileRemoval = true;
|
||||
|
||||
await RestartOrExit().ConfigureAwait(false);
|
||||
}
|
||||
|
||||
private static bool UpdateFromArchive(ZipArchive archive, string targetDirectory) {
|
||||
ArgumentNullException.ThrowIfNull(archive);
|
||||
ArgumentException.ThrowIfNullOrEmpty(targetDirectory);
|
||||
private static async Task<(bool Updated, Version? NewVersion)> UpdateASF(GlobalConfig.EUpdateChannel? channel = null, bool updateOverride = false, bool forced = false) {
|
||||
if (channel.HasValue && !Enum.IsDefined(channel.Value)) {
|
||||
throw new InvalidEnumArgumentException(nameof(channel), (int) channel, typeof(GlobalConfig.EUpdateChannel));
|
||||
}
|
||||
|
||||
if (GlobalConfig == null) {
|
||||
throw new InvalidOperationException(nameof(GlobalConfig));
|
||||
}
|
||||
|
||||
if (WebBrowser == null) {
|
||||
throw new InvalidOperationException(nameof(WebBrowser));
|
||||
}
|
||||
|
||||
channel ??= GlobalConfig.UpdateChannel;
|
||||
|
||||
if (!SharedInfo.BuildInfo.CanUpdate || (channel == GlobalConfig.EUpdateChannel.None)) {
|
||||
return (false, null);
|
||||
}
|
||||
|
||||
string targetFile;
|
||||
|
||||
await UpdateSemaphore.WaitAsync().ConfigureAwait(false);
|
||||
|
||||
try {
|
||||
// If backup directory from previous update exists, it's a good idea to purge it now
|
||||
string backupDirectory = Path.Combine(SharedInfo.HomeDirectory, SharedInfo.UpdateDirectory);
|
||||
|
||||
if (Directory.Exists(backupDirectory)) {
|
||||
ArchiLogger.LogGenericInfo(Strings.UpdateCleanup);
|
||||
|
||||
for (byte i = 0; (i < WebBrowser.MaxTries) && Directory.Exists(backupDirectory); i++) {
|
||||
if (i > 0) {
|
||||
// It's entirely possible that old process is still running, wait a short moment for eventual cleanup
|
||||
await Task.Delay(5000).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
try {
|
||||
Directory.Delete(backupDirectory, true);
|
||||
} catch (Exception e) {
|
||||
ArchiLogger.LogGenericDebuggingException(e);
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
if (Directory.Exists(backupDirectory)) {
|
||||
ArchiLogger.LogGenericError(Strings.WarningFailed);
|
||||
|
||||
return (false, null);
|
||||
}
|
||||
|
||||
ArchiLogger.LogGenericInfo(Strings.Done);
|
||||
}
|
||||
|
||||
ArchiLogger.LogGenericInfo(Strings.UpdateCheckingNewVersion);
|
||||
|
||||
ReleaseResponse? releaseResponse = await GitHubService.GetLatestRelease(SharedInfo.GithubRepo, channel == GlobalConfig.EUpdateChannel.Stable).ConfigureAwait(false);
|
||||
|
||||
if (releaseResponse == null) {
|
||||
ArchiLogger.LogGenericWarning(Strings.ErrorUpdateCheckFailed);
|
||||
|
||||
return (false, null);
|
||||
}
|
||||
|
||||
if (string.IsNullOrEmpty(releaseResponse.Tag)) {
|
||||
ArchiLogger.LogGenericWarning(Strings.ErrorUpdateCheckFailed);
|
||||
|
||||
return (false, null);
|
||||
}
|
||||
|
||||
Version newVersion = new(releaseResponse.Tag);
|
||||
|
||||
ArchiLogger.LogGenericInfo(string.Format(CultureInfo.CurrentCulture, Strings.UpdateVersionInfo, SharedInfo.Version, newVersion));
|
||||
|
||||
if (!forced && (SharedInfo.Version >= newVersion)) {
|
||||
return (false, newVersion);
|
||||
}
|
||||
|
||||
if (!updateOverride && (GlobalConfig.UpdatePeriod == 0)) {
|
||||
ArchiLogger.LogGenericInfo(Strings.UpdateNewVersionAvailable);
|
||||
await Task.Delay(SharedInfo.ShortInformationDelay).ConfigureAwait(false);
|
||||
|
||||
return (false, newVersion);
|
||||
}
|
||||
|
||||
// Auto update logic starts here
|
||||
if (releaseResponse.Assets.IsEmpty) {
|
||||
ArchiLogger.LogGenericWarning(Strings.ErrorUpdateNoAssets);
|
||||
|
||||
return (false, newVersion);
|
||||
}
|
||||
|
||||
targetFile = $"{SharedInfo.ASF}-{SharedInfo.BuildInfo.Variant}.zip";
|
||||
ReleaseAsset? binaryAsset = releaseResponse.Assets.FirstOrDefault(asset => !string.IsNullOrEmpty(asset.Name) && asset.Name.Equals(targetFile, StringComparison.OrdinalIgnoreCase));
|
||||
|
||||
if (binaryAsset == null) {
|
||||
ArchiLogger.LogGenericWarning(Strings.ErrorUpdateNoAssetForThisVersion);
|
||||
|
||||
return (false, newVersion);
|
||||
}
|
||||
|
||||
ArchiLogger.LogGenericInfo(Strings.FetchingChecksumFromRemoteServer);
|
||||
|
||||
string? remoteChecksum = await ArchiNet.FetchBuildChecksum(newVersion, SharedInfo.BuildInfo.Variant).ConfigureAwait(false);
|
||||
|
||||
switch (remoteChecksum) {
|
||||
case null:
|
||||
// Timeout or error, refuse to update as a security measure
|
||||
return (false, newVersion);
|
||||
case "":
|
||||
// Unknown checksum, release too new or actual malicious build published, no need to scare the user as it's 99.99% the first
|
||||
ArchiLogger.LogGenericWarning(Strings.ChecksumMissing);
|
||||
|
||||
return (false, newVersion);
|
||||
}
|
||||
|
||||
if (!string.IsNullOrEmpty(releaseResponse.ChangelogPlainText)) {
|
||||
ArchiLogger.LogGenericInfo(releaseResponse.ChangelogPlainText);
|
||||
}
|
||||
|
||||
ArchiLogger.LogGenericInfo(string.Format(CultureInfo.CurrentCulture, Strings.UpdateDownloadingNewVersion, newVersion, binaryAsset.Size / 1024 / 1024));
|
||||
|
||||
Progress<byte> progressReporter = new();
|
||||
|
||||
progressReporter.ProgressChanged += onProgressChanged;
|
||||
|
||||
BinaryResponse? response;
|
||||
|
||||
try {
|
||||
response = await WebBrowser.UrlGetToBinary(binaryAsset.DownloadURL, progressReporter: progressReporter).ConfigureAwait(false);
|
||||
} finally {
|
||||
progressReporter.ProgressChanged -= onProgressChanged;
|
||||
}
|
||||
|
||||
if (response?.Content == null) {
|
||||
return (false, newVersion);
|
||||
}
|
||||
|
||||
ArchiLogger.LogGenericInfo(Strings.VerifyingChecksumWithRemoteServer);
|
||||
|
||||
byte[] responseBytes = response.Content as byte[] ?? response.Content.ToArray();
|
||||
|
||||
string checksum = Utilities.GenerateChecksumFor(responseBytes);
|
||||
|
||||
if (!checksum.Equals(remoteChecksum, StringComparison.OrdinalIgnoreCase)) {
|
||||
ArchiLogger.LogGenericError(Strings.ChecksumWrong);
|
||||
|
||||
return (false, newVersion);
|
||||
}
|
||||
|
||||
await PluginsCore.OnUpdateProceeding(newVersion).ConfigureAwait(false);
|
||||
|
||||
bool kestrelWasRunning = ArchiKestrel.IsRunning;
|
||||
|
||||
if (kestrelWasRunning) {
|
||||
// We disable ArchiKestrel here as the update process moves the core files and might result in IPC crash
|
||||
// TODO: It might fail if the update was triggered from the API, this should be something to improve in the future, by changing the structure into request -> return response -> finish update
|
||||
try {
|
||||
await ArchiKestrel.Stop().ConfigureAwait(false);
|
||||
} catch (Exception e) {
|
||||
ArchiLogger.LogGenericWarningException(e);
|
||||
}
|
||||
}
|
||||
|
||||
ArchiLogger.LogGenericInfo(Strings.PatchingFiles);
|
||||
|
||||
try {
|
||||
MemoryStream memoryStream = new(responseBytes);
|
||||
|
||||
await using (memoryStream.ConfigureAwait(false)) {
|
||||
using ZipArchive zipArchive = new(memoryStream);
|
||||
|
||||
if (!await UpdateFromArchive(newVersion, channel.Value, updateOverride, forced, zipArchive).ConfigureAwait(false)) {
|
||||
ArchiLogger.LogGenericError(Strings.WarningFailed);
|
||||
}
|
||||
}
|
||||
} catch (Exception e) {
|
||||
ArchiLogger.LogGenericException(e);
|
||||
|
||||
if (kestrelWasRunning) {
|
||||
// We've temporarily disabled ArchiKestrel but the update has failed, let's bring it back up
|
||||
// We can't even be sure if it's possible to bring it back up in this state, but it's worth trying anyway
|
||||
try {
|
||||
await ArchiKestrel.Start().ConfigureAwait(false);
|
||||
} catch (Exception ex) {
|
||||
ArchiLogger.LogGenericWarningException(ex);
|
||||
}
|
||||
}
|
||||
|
||||
return (false, newVersion);
|
||||
}
|
||||
|
||||
ArchiLogger.LogGenericInfo(Strings.UpdateFinished);
|
||||
|
||||
await PluginsCore.OnUpdateFinished(newVersion).ConfigureAwait(false);
|
||||
|
||||
return (true, newVersion);
|
||||
} finally {
|
||||
UpdateSemaphore.Release();
|
||||
}
|
||||
|
||||
void onProgressChanged(object? sender, byte progressPercentage) {
|
||||
ArgumentOutOfRangeException.ThrowIfGreaterThan(progressPercentage, 100);
|
||||
|
||||
Utilities.OnProgressChanged(targetFile, progressPercentage);
|
||||
}
|
||||
}
|
||||
|
||||
private static async Task<bool> UpdateFromArchive(Version newVersion, GlobalConfig.EUpdateChannel updateChannel, bool updateOverride, bool forced, ZipArchive zipArchive) {
|
||||
ArgumentNullException.ThrowIfNull(newVersion);
|
||||
|
||||
if (!Enum.IsDefined(updateChannel)) {
|
||||
throw new InvalidEnumArgumentException(nameof(updateChannel), (int) updateChannel, typeof(GlobalConfig.EUpdateChannel));
|
||||
}
|
||||
|
||||
ArgumentNullException.ThrowIfNull(zipArchive);
|
||||
|
||||
if (SharedInfo.HomeDirectory == AppContext.BaseDirectory) {
|
||||
// We're running a build that includes our dependencies in ASF's home
|
||||
@@ -923,114 +985,10 @@ public static class ASF {
|
||||
LoadAssembliesNeededBeforeUpdate();
|
||||
}
|
||||
|
||||
// Firstly we'll move all our existing files to a backup directory
|
||||
string backupDirectory = Path.Combine(targetDirectory, SharedInfo.UpdateDirectory);
|
||||
// We're ready to start update process, handle any plugin updates ready for new version
|
||||
await PluginsCore.UpdatePlugins(newVersion, true, updateChannel, updateOverride, forced).ConfigureAwait(false);
|
||||
|
||||
foreach (string file in Directory.EnumerateFiles(targetDirectory, "*", SearchOption.AllDirectories)) {
|
||||
string fileName = Path.GetFileName(file);
|
||||
|
||||
if (string.IsNullOrEmpty(fileName)) {
|
||||
ArchiLogger.LogNullError(fileName);
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
string relativeFilePath = Path.GetRelativePath(targetDirectory, file);
|
||||
|
||||
if (string.IsNullOrEmpty(relativeFilePath)) {
|
||||
ArchiLogger.LogNullError(relativeFilePath);
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
string? relativeDirectoryName = Path.GetDirectoryName(relativeFilePath);
|
||||
|
||||
switch (relativeDirectoryName) {
|
||||
case null:
|
||||
ArchiLogger.LogNullError(relativeDirectoryName);
|
||||
|
||||
return false;
|
||||
case "":
|
||||
// No directory, root folder
|
||||
switch (fileName) {
|
||||
case Logging.NLogConfigurationFile:
|
||||
case SharedInfo.LogFile:
|
||||
// Files with those names in root directory we want to keep
|
||||
continue;
|
||||
}
|
||||
|
||||
break;
|
||||
case SharedInfo.ArchivalLogsDirectory:
|
||||
case SharedInfo.ConfigDirectory:
|
||||
case SharedInfo.DebugDirectory:
|
||||
case SharedInfo.PluginsDirectory:
|
||||
case SharedInfo.UpdateDirectory:
|
||||
// Files in those directories we want to keep in their current place
|
||||
continue;
|
||||
default:
|
||||
// Files in subdirectories of those directories we want to keep as well
|
||||
if (Utilities.RelativeDirectoryStartsWith(relativeDirectoryName, SharedInfo.ArchivalLogsDirectory, SharedInfo.ConfigDirectory, SharedInfo.DebugDirectory, SharedInfo.PluginsDirectory, SharedInfo.UpdateDirectory)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
string targetBackupDirectory = relativeDirectoryName.Length > 0 ? Path.Combine(backupDirectory, relativeDirectoryName) : backupDirectory;
|
||||
Directory.CreateDirectory(targetBackupDirectory);
|
||||
|
||||
string targetBackupFile = Path.Combine(targetBackupDirectory, fileName);
|
||||
|
||||
File.Move(file, targetBackupFile, true);
|
||||
}
|
||||
|
||||
// We can now get rid of directories that are empty
|
||||
Utilities.DeleteEmptyDirectoriesRecursively(targetDirectory);
|
||||
|
||||
if (!Directory.Exists(targetDirectory)) {
|
||||
Directory.CreateDirectory(targetDirectory);
|
||||
}
|
||||
|
||||
// Now enumerate over files in the zip archive, skip directory entries that we're not interested in (we can create them ourselves if needed)
|
||||
foreach (ZipArchiveEntry zipFile in archive.Entries.Where(static zipFile => !string.IsNullOrEmpty(zipFile.Name))) {
|
||||
string file = Path.GetFullPath(Path.Combine(targetDirectory, zipFile.FullName));
|
||||
|
||||
if (!file.StartsWith(targetDirectory, StringComparison.Ordinal)) {
|
||||
throw new InvalidOperationException(nameof(file));
|
||||
}
|
||||
|
||||
if (File.Exists(file)) {
|
||||
// This is possible only with files that we decided to leave in place during our backup function
|
||||
string targetBackupFile = $"{file}.bak";
|
||||
|
||||
File.Move(file, targetBackupFile, true);
|
||||
}
|
||||
|
||||
// Check if this file requires its own folder
|
||||
if (zipFile.Name != zipFile.FullName) {
|
||||
string? directory = Path.GetDirectoryName(file);
|
||||
|
||||
if (string.IsNullOrEmpty(directory)) {
|
||||
ArchiLogger.LogNullError(directory);
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!Directory.Exists(directory)) {
|
||||
Directory.CreateDirectory(directory);
|
||||
}
|
||||
|
||||
// We're not interested in extracting placeholder files (but we still want directories created for them, done above)
|
||||
switch (zipFile.Name) {
|
||||
case ".gitkeep":
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
zipFile.ExtractToFile(file);
|
||||
}
|
||||
|
||||
return true;
|
||||
return Utilities.UpdateFromArchive(zipArchive, SharedInfo.HomeDirectory);
|
||||
}
|
||||
|
||||
[PublicAPI]
|
||||
@@ -1047,6 +1005,7 @@ public static class ASF {
|
||||
|
||||
internal enum EFileType : byte {
|
||||
Config,
|
||||
Database
|
||||
Database,
|
||||
Crash
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
// _ _ _ ____ _ _____
|
||||
// ----------------------------------------------------------------------------------------------
|
||||
// _ _ _ ____ _ _____
|
||||
// / \ _ __ ___ | |__ (_)/ ___| | |_ ___ __ _ _ __ ___ | ___|__ _ _ __ _ __ ___
|
||||
// / _ \ | '__|/ __|| '_ \ | |\___ \ | __|/ _ \ / _` || '_ ` _ \ | |_ / _` || '__|| '_ ` _ \
|
||||
// / ___ \ | | | (__ | | | || | ___) || |_| __/| (_| || | | | | || _|| (_| || | | | | | | |
|
||||
// /_/ \_\|_| \___||_| |_||_||____/ \__|\___| \__,_||_| |_| |_||_| \__,_||_| |_| |_| |_|
|
||||
// ----------------------------------------------------------------------------------------------
|
||||
// |
|
||||
// Copyright 2015-2023 Łukasz "JustArchi" Domeradzki
|
||||
// Copyright 2015-2024 Łukasz "JustArchi" Domeradzki
|
||||
// Contact: JustArchi@JustArchi.net
|
||||
// |
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
// ----------------------------------------------------------------------------------------------
|
||||
// _ _ _ ____ _ _____
|
||||
// / \ _ __ ___ | |__ (_)/ ___| | |_ ___ __ _ _ __ ___ | ___|__ _ _ __ _ __ ___
|
||||
// / _ \ | '__|/ __|| '_ \ | |\___ \ | __|/ _ \ / _` || '_ ` _ \ | |_ / _` || '__|| '_ ` _ \
|
||||
// / ___ \ | | | (__ | | | || | ___) || |_| __/| (_| || | | | | || _|| (_| || | | | | | | |
|
||||
// /_/ \_\|_| \___||_| |_||_||____/ \__|\___| \__,_||_| |_| |_||_| \__,_||_| |_| |_| |_|
|
||||
// ----------------------------------------------------------------------------------------------
|
||||
// |
|
||||
// Copyright 2015-2023 Łukasz "JustArchi" Domeradzki
|
||||
// Copyright 2015-2024 Łukasz "JustArchi" Domeradzki
|
||||
// Contact: JustArchi@JustArchi.net
|
||||
// |
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
// ----------------------------------------------------------------------------------------------
|
||||
// _ _ _ ____ _ _____
|
||||
// / \ _ __ ___ | |__ (_)/ ___| | |_ ___ __ _ _ __ ___ | ___|__ _ _ __ _ __ ___
|
||||
// / _ \ | '__|/ __|| '_ \ | |\___ \ | __|/ _ \ / _` || '_ ` _ \ | |_ / _` || '__|| '_ ` _ \
|
||||
// / ___ \ | | | (__ | | | || | ___) || |_| __/| (_| || | | | | || _|| (_| || | | | | | | |
|
||||
// /_/ \_\|_| \___||_| |_||_||____/ \__|\___| \__,_||_| |_| |_||_| \__,_||_| |_| |_| |_|
|
||||
// ----------------------------------------------------------------------------------------------
|
||||
// |
|
||||
// Copyright 2015-2023 Łukasz "JustArchi" Domeradzki
|
||||
// Copyright 2015-2024 Łukasz "JustArchi" Domeradzki
|
||||
// Contact: JustArchi@JustArchi.net
|
||||
// |
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
// ----------------------------------------------------------------------------------------------
|
||||
// _ _ _ ____ _ _____
|
||||
// / \ _ __ ___ | |__ (_)/ ___| | |_ ___ __ _ _ __ ___ | ___|__ _ _ __ _ __ ___
|
||||
// / _ \ | '__|/ __|| '_ \ | |\___ \ | __|/ _ \ / _` || '_ ` _ \ | |_ / _` || '__|| '_ ` _ \
|
||||
// / ___ \ | | | (__ | | | || | ___) || |_| __/| (_| || | | | | || _|| (_| || | | | | | | |
|
||||
// /_/ \_\|_| \___||_| |_||_||____/ \__|\___| \__,_||_| |_| |_||_| \__,_||_| |_| |_| |_|
|
||||
// ----------------------------------------------------------------------------------------------
|
||||
// |
|
||||
// Copyright 2015-2023 Łukasz "JustArchi" Domeradzki
|
||||
// Copyright 2015-2024 Łukasz "JustArchi" Domeradzki
|
||||
// Contact: JustArchi@JustArchi.net
|
||||
// |
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
// ----------------------------------------------------------------------------------------------
|
||||
// _ _ _ ____ _ _____
|
||||
// / \ _ __ ___ | |__ (_)/ ___| | |_ ___ __ _ _ __ ___ | ___|__ _ _ __ _ __ ___
|
||||
// / _ \ | '__|/ __|| '_ \ | |\___ \ | __|/ _ \ / _` || '_ ` _ \ | |_ / _` || '__|| '_ ` _ \
|
||||
// / ___ \ | | | (__ | | | || | ___) || |_| __/| (_| || | | | | || _|| (_| || | | | | | | |
|
||||
// /_/ \_\|_| \___||_| |_||_||____/ \__|\___| \__,_||_| |_| |_||_| \__,_||_| |_| |_| |_|
|
||||
// ----------------------------------------------------------------------------------------------
|
||||
// |
|
||||
// Copyright 2015-2023 Łukasz "JustArchi" Domeradzki
|
||||
// Copyright 2015-2024 Łukasz "JustArchi" Domeradzki
|
||||
// Contact: JustArchi@JustArchi.net
|
||||
// |
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
// ----------------------------------------------------------------------------------------------
|
||||
// _ _ _ ____ _ _____
|
||||
// / \ _ __ ___ | |__ (_)/ ___| | |_ ___ __ _ _ __ ___ | ___|__ _ _ __ _ __ ___
|
||||
// / _ \ | '__|/ __|| '_ \ | |\___ \ | __|/ _ \ / _` || '_ ` _ \ | |_ / _` || '__|| '_ ` _ \
|
||||
// / ___ \ | | | (__ | | | || | ___) || |_| __/| (_| || | | | | || _|| (_| || | | | | | | |
|
||||
// /_/ \_\|_| \___||_| |_||_||____/ \__|\___| \__,_||_| |_| |_||_| \__,_||_| |_| |_| |_|
|
||||
// ----------------------------------------------------------------------------------------------
|
||||
// |
|
||||
// Copyright 2015-2023 Łukasz "JustArchi" Domeradzki
|
||||
// Copyright 2015-2024 Łukasz "JustArchi" Domeradzki
|
||||
// Contact: JustArchi@JustArchi.net
|
||||
// |
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
@@ -26,6 +28,12 @@ using System.Runtime.Versioning;
|
||||
namespace ArchiSteamFarm.Core;
|
||||
|
||||
internal static partial class NativeMethods {
|
||||
[DefaultDllImportSearchPaths(DllImportSearchPath.System32)]
|
||||
[SupportedOSPlatform("Windows")]
|
||||
[return: MarshalAs(UnmanagedType.Bool)]
|
||||
[LibraryImport("user32.dll")]
|
||||
internal static partial void FlashWindowEx(ref FlashWindowInfo pwfi);
|
||||
|
||||
[DefaultDllImportSearchPaths(DllImportSearchPath.System32)]
|
||||
[SupportedOSPlatform("Windows")]
|
||||
[return: MarshalAs(UnmanagedType.Bool)]
|
||||
@@ -77,6 +85,16 @@ internal static partial class NativeMethods {
|
||||
Awake = SystemRequired | AwayModeRequired | Continuous
|
||||
}
|
||||
|
||||
[SupportedOSPlatform("Windows")]
|
||||
[Flags]
|
||||
internal enum EFlashFlags : uint {
|
||||
Stop = 0,
|
||||
Caption = 1,
|
||||
Tray = 2,
|
||||
All = Caption | Tray,
|
||||
Timer = 4
|
||||
}
|
||||
|
||||
[SupportedOSPlatform("Windows")]
|
||||
internal enum EShowWindow : uint {
|
||||
Minimize = 6
|
||||
@@ -86,4 +104,15 @@ internal static partial class NativeMethods {
|
||||
internal enum EStandardHandle {
|
||||
Input = -10
|
||||
}
|
||||
|
||||
[StructLayout(LayoutKind.Sequential)]
|
||||
internal struct FlashWindowInfo {
|
||||
#pragma warning disable Reordering // TODO: This silly pragma doesn't do anything, but it stops Rider from reordering, we may be able to get rid of it later
|
||||
public uint StructSize;
|
||||
public nint WindowHandle;
|
||||
public EFlashFlags Flags;
|
||||
public uint Count;
|
||||
public uint TimeoutBetweenFlashes;
|
||||
#pragma warning restore Reordering // TODO: This silly pragma doesn't do anything, but it stops Rider from reordering, we may be able to get rid of it later
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
// ----------------------------------------------------------------------------------------------
|
||||
// _ _ _ ____ _ _____
|
||||
// / \ _ __ ___ | |__ (_)/ ___| | |_ ___ __ _ _ __ ___ | ___|__ _ _ __ _ __ ___
|
||||
// / _ \ | '__|/ __|| '_ \ | |\___ \ | __|/ _ \ / _` || '_ ` _ \ | |_ / _` || '__|| '_ ` _ \
|
||||
// / ___ \ | | | (__ | | | || | ___) || |_| __/| (_| || | | | | || _|| (_| || | | | | | | |
|
||||
// /_/ \_\|_| \___||_| |_||_||____/ \__|\___| \__,_||_| |_| |_||_| \__,_||_| |_| |_| |_|
|
||||
// ----------------------------------------------------------------------------------------------
|
||||
// |
|
||||
// Copyright 2015-2023 Łukasz "JustArchi" Domeradzki
|
||||
// Copyright 2015-2024 Łukasz "JustArchi" Domeradzki
|
||||
// Contact: JustArchi@JustArchi.net
|
||||
// |
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
@@ -84,11 +86,11 @@ internal static class OS {
|
||||
private static Mutex? SingleInstance;
|
||||
|
||||
internal static void CoreInit(bool minimized, bool systemRequired) {
|
||||
if (OperatingSystem.IsWindows()) {
|
||||
if (minimized) {
|
||||
WindowsMinimizeConsoleWindow();
|
||||
}
|
||||
if (minimized) {
|
||||
MinimizeConsoleWindow();
|
||||
}
|
||||
|
||||
if (OperatingSystem.IsWindows()) {
|
||||
if (systemRequired) {
|
||||
WindowsKeepSystemActive();
|
||||
}
|
||||
@@ -227,6 +229,73 @@ internal static class OS {
|
||||
return false;
|
||||
}
|
||||
|
||||
[SupportedOSPlatform("Windows")]
|
||||
internal static void WindowsStartFlashingConsoleWindow() {
|
||||
if (!OperatingSystem.IsWindows()) {
|
||||
throw new PlatformNotSupportedException();
|
||||
}
|
||||
|
||||
using Process currentProcess = Process.GetCurrentProcess();
|
||||
|
||||
nint handle = currentProcess.MainWindowHandle;
|
||||
|
||||
if (handle == nint.Zero) {
|
||||
return;
|
||||
}
|
||||
|
||||
NativeMethods.FlashWindowInfo flashInfo = new() {
|
||||
StructSize = (uint) Marshal.SizeOf<NativeMethods.FlashWindowInfo>(),
|
||||
Flags = NativeMethods.EFlashFlags.All | NativeMethods.EFlashFlags.Timer,
|
||||
WindowHandle = handle,
|
||||
Count = uint.MaxValue
|
||||
};
|
||||
|
||||
NativeMethods.FlashWindowEx(ref flashInfo);
|
||||
}
|
||||
|
||||
[SupportedOSPlatform("Windows")]
|
||||
internal static void WindowsStopFlashingConsoleWindow() {
|
||||
if (!OperatingSystem.IsWindows()) {
|
||||
throw new PlatformNotSupportedException();
|
||||
}
|
||||
|
||||
using Process currentProcess = Process.GetCurrentProcess();
|
||||
nint handle = currentProcess.MainWindowHandle;
|
||||
|
||||
if (handle == nint.Zero) {
|
||||
return;
|
||||
}
|
||||
|
||||
NativeMethods.FlashWindowInfo flashInfo = new() {
|
||||
StructSize = (uint) Marshal.SizeOf<NativeMethods.FlashWindowInfo>(),
|
||||
Flags = NativeMethods.EFlashFlags.Stop,
|
||||
WindowHandle = handle
|
||||
};
|
||||
|
||||
NativeMethods.FlashWindowEx(ref flashInfo);
|
||||
}
|
||||
|
||||
private static void MinimizeConsoleWindow() {
|
||||
(_, int top) = Console.GetCursorPosition();
|
||||
|
||||
// Will work if the terminal supports XTWINOPS "iconify" escape sequence, reference: https://invisible-island.net/xterm/ctlseqs/ctlseqs.html
|
||||
Console.Write('\x1b' + @"[2;0;0t");
|
||||
|
||||
// Reset cursor position if terminal outputs escape sequences as-is
|
||||
Console.SetCursorPosition(0, top);
|
||||
|
||||
// Fallback if we're using conhost on Windows
|
||||
if (OperatingSystem.IsWindows()) {
|
||||
using Process process = Process.GetCurrentProcess();
|
||||
|
||||
nint windowHandle = process.MainWindowHandle;
|
||||
|
||||
if (windowHandle != nint.Zero) {
|
||||
NativeMethods.ShowWindow(windowHandle, NativeMethods.EShowWindow.Minimize);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
[SupportedOSPlatform("Windows")]
|
||||
private static void WindowsDisableQuickEditMode() {
|
||||
if (!OperatingSystem.IsWindows()) {
|
||||
@@ -264,15 +333,4 @@ internal static class OS {
|
||||
ASF.ArchiLogger.LogGenericError(string.Format(CultureInfo.CurrentCulture, Strings.WarningFailedWithError, result));
|
||||
}
|
||||
}
|
||||
|
||||
[SupportedOSPlatform("Windows")]
|
||||
private static void WindowsMinimizeConsoleWindow() {
|
||||
if (!OperatingSystem.IsWindows()) {
|
||||
throw new PlatformNotSupportedException();
|
||||
}
|
||||
|
||||
using Process process = Process.GetCurrentProcess();
|
||||
|
||||
NativeMethods.ShowWindow(process.MainWindowHandle, NativeMethods.EShowWindow.Minimize);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
// ----------------------------------------------------------------------------------------------
|
||||
// _ _ _ ____ _ _____
|
||||
// / \ _ __ ___ | |__ (_)/ ___| | |_ ___ __ _ _ __ ___ | ___|__ _ _ __ _ __ ___
|
||||
// / _ \ | '__|/ __|| '_ \ | |\___ \ | __|/ _ \ / _` || '_ ` _ \ | |_ / _` || '__|| '_ ` _ \
|
||||
// / ___ \ | | | (__ | | | || | ___) || |_| __/| (_| || | | | | || _|| (_| || | | | | | | |
|
||||
// /_/ \_\|_| \___||_| |_||_||____/ \__|\___| \__,_||_| |_| |_||_| \__,_||_| |_| |_| |_|
|
||||
// ----------------------------------------------------------------------------------------------
|
||||
// |
|
||||
// Copyright 2015-2023 Łukasz "JustArchi" Domeradzki
|
||||
// Copyright 2015-2024 Łukasz "JustArchi" Domeradzki
|
||||
// Contact: JustArchi@JustArchi.net
|
||||
// |
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
@@ -21,11 +23,12 @@
|
||||
|
||||
using System;
|
||||
using System.Collections;
|
||||
using System.Collections.Frozen;
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.Immutable;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.Globalization;
|
||||
using System.IdentityModel.Tokens.Jwt;
|
||||
using System.IO;
|
||||
using System.IO.Compression;
|
||||
using System.Linq;
|
||||
using System.Net;
|
||||
using System.Resources;
|
||||
@@ -35,10 +38,12 @@ using System.Threading.Tasks;
|
||||
using AngleSharp.Dom;
|
||||
using AngleSharp.XPath;
|
||||
using ArchiSteamFarm.Localization;
|
||||
using ArchiSteamFarm.NLog;
|
||||
using ArchiSteamFarm.Storage;
|
||||
using Humanizer;
|
||||
using Humanizer.Localisation;
|
||||
using JetBrains.Annotations;
|
||||
using Microsoft.IdentityModel.JsonWebTokens;
|
||||
using SteamKit2;
|
||||
using Zxcvbn;
|
||||
|
||||
@@ -48,9 +53,7 @@ public static class Utilities {
|
||||
private const byte TimeoutForLongRunningTasksInSeconds = 60;
|
||||
|
||||
// normally we'd just use words like "steam" and "farm", but the library we're currently using is a bit iffy about banned words, so we need to also add combinations such as "steamfarm"
|
||||
private static readonly ImmutableHashSet<string> ForbiddenPasswordPhrases = ImmutableHashSet.Create(StringComparer.InvariantCultureIgnoreCase, "archisteamfarm", "archi", "steam", "farm", "archisteam", "archifarm", "steamfarm", "asf", "asffarm", "password");
|
||||
|
||||
private static readonly JwtSecurityTokenHandler JwtSecurityTokenHandler = new();
|
||||
private static readonly FrozenSet<string> ForbiddenPasswordPhrases = new HashSet<string>(10, StringComparer.InvariantCultureIgnoreCase) { "archisteamfarm", "archi", "steam", "farm", "archisteam", "archifarm", "steamfarm", "asf", "asffarm", "password" }.ToFrozenSet(StringComparer.InvariantCultureIgnoreCase);
|
||||
|
||||
[PublicAPI]
|
||||
public static string GenerateChecksumFor(byte[] source) {
|
||||
@@ -123,7 +126,7 @@ public static class Utilities {
|
||||
|
||||
switch (ASF.GlobalConfig?.OptimizationMode) {
|
||||
case GlobalConfig.EOptimizationMode.MinMemoryUsage:
|
||||
List<T> results = new();
|
||||
List<T> results = [];
|
||||
|
||||
foreach (Task<T> task in tasks) {
|
||||
results.Add(await task.ConfigureAwait(false));
|
||||
@@ -179,19 +182,6 @@ public static class Utilities {
|
||||
return (text.Length % 2 == 0) && text.All(Uri.IsHexDigit);
|
||||
}
|
||||
|
||||
[PublicAPI]
|
||||
public static JwtSecurityToken? ReadJwtToken(string token) {
|
||||
ArgumentException.ThrowIfNullOrEmpty(token);
|
||||
|
||||
try {
|
||||
return JwtSecurityTokenHandler.ReadJwtToken(token);
|
||||
} catch (Exception e) {
|
||||
ASF.ArchiLogger.LogGenericException(e);
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
[PublicAPI]
|
||||
public static IList<INode> SelectNodes(this IDocument document, string xpath) {
|
||||
ArgumentNullException.ThrowIfNull(document);
|
||||
@@ -260,24 +250,21 @@ public static class Utilities {
|
||||
return job.ToTask();
|
||||
}
|
||||
|
||||
internal static void DeleteEmptyDirectoriesRecursively(string directory) {
|
||||
ArgumentException.ThrowIfNullOrEmpty(directory);
|
||||
|
||||
if (!Directory.Exists(directory)) {
|
||||
return;
|
||||
}
|
||||
[PublicAPI]
|
||||
public static bool TryReadJsonWebToken(string token, [NotNullWhen(true)] out JsonWebToken? result) {
|
||||
ArgumentException.ThrowIfNullOrEmpty(token);
|
||||
|
||||
try {
|
||||
foreach (string subDirectory in Directory.EnumerateDirectories(directory)) {
|
||||
DeleteEmptyDirectoriesRecursively(subDirectory);
|
||||
}
|
||||
|
||||
if (!Directory.EnumerateFileSystemEntries(directory).Any()) {
|
||||
Directory.Delete(directory);
|
||||
}
|
||||
result = new JsonWebToken(token);
|
||||
} catch (Exception e) {
|
||||
ASF.ArchiLogger.LogGenericException(e);
|
||||
ASF.ArchiLogger.LogGenericDebuggingException(e);
|
||||
|
||||
result = null;
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
internal static ulong MathAdd(ulong first, int second) {
|
||||
@@ -288,16 +275,17 @@ public static class Utilities {
|
||||
return first - (uint) -second;
|
||||
}
|
||||
|
||||
internal static bool RelativeDirectoryStartsWith(string directory, params string[] prefixes) {
|
||||
ArgumentException.ThrowIfNullOrEmpty(directory);
|
||||
internal static void OnProgressChanged(string fileName, byte progressPercentage) {
|
||||
ArgumentException.ThrowIfNullOrEmpty(fileName);
|
||||
ArgumentOutOfRangeException.ThrowIfGreaterThan(progressPercentage, 100);
|
||||
|
||||
#pragma warning disable CA1508 // False positive, params could be null when explicitly set
|
||||
if ((prefixes == null) || (prefixes.Length == 0)) {
|
||||
#pragma warning restore CA1508 // False positive, params could be null when explicitly set
|
||||
throw new ArgumentNullException(nameof(prefixes));
|
||||
const byte printEveryPercentage = 10;
|
||||
|
||||
if (progressPercentage % printEveryPercentage != 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
return (from prefix in prefixes where directory.Length > prefix.Length let pathSeparator = directory[prefix.Length] where (pathSeparator == Path.DirectorySeparatorChar) || (pathSeparator == Path.AltDirectorySeparatorChar) select prefix).Any(prefix => directory.StartsWith(prefix, StringComparison.Ordinal));
|
||||
ASF.ArchiLogger.LogGenericDebug($"{fileName} {progressPercentage}%...");
|
||||
}
|
||||
|
||||
internal static (bool IsWeak, string? Reason) TestPasswordStrength(string password, ISet<string>? additionallyForbiddenPhrases = null) {
|
||||
@@ -334,6 +322,120 @@ public static class Utilities {
|
||||
return (result.Score < 4, suggestions is { Count: > 0 } ? string.Join(' ', suggestions.Where(static suggestion => suggestion.Length > 0)) : null);
|
||||
}
|
||||
|
||||
internal static bool UpdateFromArchive(ZipArchive zipArchive, string targetDirectory) {
|
||||
ArgumentNullException.ThrowIfNull(zipArchive);
|
||||
ArgumentException.ThrowIfNullOrEmpty(targetDirectory);
|
||||
|
||||
// Firstly we'll move all our existing files to a backup directory
|
||||
string backupDirectory = Path.Combine(targetDirectory, SharedInfo.UpdateDirectory);
|
||||
|
||||
foreach (string file in Directory.EnumerateFiles(targetDirectory, "*", SearchOption.AllDirectories)) {
|
||||
string fileName = Path.GetFileName(file);
|
||||
|
||||
if (string.IsNullOrEmpty(fileName)) {
|
||||
ASF.ArchiLogger.LogNullError(fileName);
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
string relativeFilePath = Path.GetRelativePath(targetDirectory, file);
|
||||
|
||||
if (string.IsNullOrEmpty(relativeFilePath)) {
|
||||
ASF.ArchiLogger.LogNullError(relativeFilePath);
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
string? relativeDirectoryName = Path.GetDirectoryName(relativeFilePath);
|
||||
|
||||
switch (relativeDirectoryName) {
|
||||
case null:
|
||||
ASF.ArchiLogger.LogNullError(relativeDirectoryName);
|
||||
|
||||
return false;
|
||||
case "":
|
||||
// No directory, root folder
|
||||
switch (fileName) {
|
||||
case Logging.NLogConfigurationFile:
|
||||
case SharedInfo.LogFile:
|
||||
// Files with those names in root directory we want to keep
|
||||
continue;
|
||||
}
|
||||
|
||||
break;
|
||||
case SharedInfo.ArchivalLogsDirectory:
|
||||
case SharedInfo.ConfigDirectory:
|
||||
case SharedInfo.DebugDirectory:
|
||||
case SharedInfo.PluginsDirectory:
|
||||
case SharedInfo.UpdateDirectory:
|
||||
// Files in those directories we want to keep in their current place
|
||||
continue;
|
||||
default:
|
||||
// Files in subdirectories of those directories we want to keep as well
|
||||
if (RelativeDirectoryStartsWith(relativeDirectoryName, SharedInfo.ArchivalLogsDirectory, SharedInfo.ConfigDirectory, SharedInfo.DebugDirectory, SharedInfo.PluginsDirectory, SharedInfo.UpdateDirectory)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
string targetBackupDirectory = relativeDirectoryName.Length > 0 ? Path.Combine(backupDirectory, relativeDirectoryName) : backupDirectory;
|
||||
Directory.CreateDirectory(targetBackupDirectory);
|
||||
|
||||
string targetBackupFile = Path.Combine(targetBackupDirectory, fileName);
|
||||
|
||||
File.Move(file, targetBackupFile, true);
|
||||
}
|
||||
|
||||
// We can now get rid of directories that are empty
|
||||
DeleteEmptyDirectoriesRecursively(targetDirectory);
|
||||
|
||||
if (!Directory.Exists(targetDirectory)) {
|
||||
Directory.CreateDirectory(targetDirectory);
|
||||
}
|
||||
|
||||
// Now enumerate over files in the zip archive, skip directory entries that we're not interested in (we can create them ourselves if needed)
|
||||
foreach (ZipArchiveEntry zipFile in zipArchive.Entries.Where(static zipFile => !string.IsNullOrEmpty(zipFile.Name))) {
|
||||
string file = Path.GetFullPath(Path.Combine(targetDirectory, zipFile.FullName));
|
||||
|
||||
if (!file.StartsWith(targetDirectory, StringComparison.Ordinal)) {
|
||||
throw new InvalidOperationException(nameof(file));
|
||||
}
|
||||
|
||||
if (File.Exists(file)) {
|
||||
// This is possible only with files that we decided to leave in place during our backup function
|
||||
string targetBackupFile = $"{file}.bak";
|
||||
|
||||
File.Move(file, targetBackupFile, true);
|
||||
}
|
||||
|
||||
// Check if this file requires its own folder
|
||||
if (zipFile.Name != zipFile.FullName) {
|
||||
string? directory = Path.GetDirectoryName(file);
|
||||
|
||||
if (string.IsNullOrEmpty(directory)) {
|
||||
ASF.ArchiLogger.LogNullError(directory);
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!Directory.Exists(directory)) {
|
||||
Directory.CreateDirectory(directory);
|
||||
}
|
||||
|
||||
// We're not interested in extracting placeholder files (but we still want directories created for them, done above)
|
||||
switch (zipFile.Name) {
|
||||
case ".gitkeep":
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
zipFile.ExtractToFile(file);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
internal static void WarnAboutIncompleteTranslation(ResourceManager resourceManager) {
|
||||
ArgumentNullException.ThrowIfNull(resourceManager);
|
||||
|
||||
@@ -388,4 +490,38 @@ public static class Utilities {
|
||||
ASF.ArchiLogger.LogGenericInfo(string.Format(CultureInfo.CurrentCulture, Strings.TranslationIncomplete, $"{CultureInfo.CurrentUICulture.Name} ({CultureInfo.CurrentUICulture.EnglishName})", translationCompleteness.ToString("P1", CultureInfo.CurrentCulture)));
|
||||
}
|
||||
}
|
||||
|
||||
private static void DeleteEmptyDirectoriesRecursively(string directory) {
|
||||
ArgumentException.ThrowIfNullOrEmpty(directory);
|
||||
|
||||
if (!Directory.Exists(directory)) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
foreach (string subDirectory in Directory.EnumerateDirectories(directory)) {
|
||||
DeleteEmptyDirectoriesRecursively(subDirectory);
|
||||
}
|
||||
|
||||
if (!Directory.EnumerateFileSystemEntries(directory).Any()) {
|
||||
Directory.Delete(directory);
|
||||
}
|
||||
} catch (Exception e) {
|
||||
ASF.ArchiLogger.LogGenericException(e);
|
||||
}
|
||||
}
|
||||
|
||||
private static bool RelativeDirectoryStartsWith(string directory, params string[] prefixes) {
|
||||
ArgumentException.ThrowIfNullOrEmpty(directory);
|
||||
|
||||
#pragma warning disable CA1508 // False positive, params could be null when explicitly set
|
||||
if ((prefixes == null) || (prefixes.Length == 0)) {
|
||||
#pragma warning restore CA1508 // False positive, params could be null when explicitly set
|
||||
throw new ArgumentNullException(nameof(prefixes));
|
||||
}
|
||||
|
||||
HashSet<char> separators = [Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar];
|
||||
|
||||
return prefixes.Where(prefix => (directory.Length > prefix.Length) && separators.Contains(directory[prefix.Length])).Any(prefix => directory.StartsWith(prefix, StringComparison.Ordinal));
|
||||
}
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user