mirror of
https://github.com/JustArchiNET/ArchiSteamFarm.git
synced 2025-12-16 06:20:34 +00:00
Compare commits
724 Commits
6.1.2.3
...
4a9ecc52c1
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4a9ecc52c1 | ||
|
|
10fc914ac1 | ||
|
|
7e9c49cc79 | ||
|
|
ab58886d29 | ||
|
|
a5740a43db | ||
|
|
223f2b98cb | ||
|
|
69b150c8a0 | ||
|
|
f4ea1d44ac | ||
|
|
d02504b914 | ||
|
|
c363af8c3a | ||
|
|
c06a22ec93 | ||
|
|
ed19d7e3bf | ||
|
|
4eee2e2ac7 | ||
|
|
90fe1a9448 | ||
|
|
8c26dcb7c2 | ||
|
|
ba8150b27a | ||
|
|
612b3dd3ef | ||
|
|
a551568f2b | ||
|
|
31615ef64d | ||
|
|
0f0f1bfcd3 | ||
|
|
702e27c466 | ||
|
|
921263674d | ||
|
|
5110d6d9f9 | ||
|
|
5f622128e4 | ||
|
|
6eee3fb4d9 | ||
|
|
0aa777734a | ||
|
|
be789d706b | ||
|
|
03bfc169bb | ||
|
|
12d26bfd60 | ||
|
|
cb31836c34 | ||
|
|
f091707b0d | ||
|
|
f4467fcd38 | ||
|
|
f00b6bfdf1 | ||
|
|
bbfe0e40c5 | ||
|
|
e1e928c4ca | ||
|
|
29cb094430 | ||
|
|
dfd2bd9ac4 | ||
|
|
c65ffa44d9 | ||
|
|
cf84c19a1a | ||
|
|
252e498d8b | ||
|
|
1e3a5a5176 | ||
|
|
c21ca7dbb3 | ||
|
|
f7b5699172 | ||
|
|
2793775f26 | ||
|
|
ab54040d3b | ||
|
|
36225c5b93 | ||
|
|
647dcb5834 | ||
|
|
12e59a649a | ||
|
|
f85c36e8ee | ||
|
|
7e4878a3f1 | ||
|
|
35aeee1b63 | ||
|
|
6f1dffbc01 | ||
|
|
f6ae839a2f | ||
|
|
4d97862ec8 | ||
|
|
84a0b59aba | ||
|
|
1c9a3dbf36 | ||
|
|
4540b4a2da | ||
|
|
1e814a63cc | ||
|
|
f4f70ec802 | ||
|
|
41656011a6 | ||
|
|
f061be0798 | ||
|
|
c0a253213d | ||
|
|
67757178c1 | ||
|
|
03350ce879 | ||
|
|
55bc802d59 | ||
|
|
a436c053a3 | ||
|
|
8547097e78 | ||
|
|
a8d9844b01 | ||
|
|
867b7270ec | ||
|
|
970998fb4b | ||
|
|
1ac4dfd6c8 | ||
|
|
ea2bec2f3e | ||
|
|
ee0eef3761 | ||
|
|
fc7ac69fb1 | ||
|
|
a32e7639ed | ||
|
|
c8e8cd27b8 | ||
|
|
245daec72e | ||
|
|
02d18aecf4 | ||
|
|
469dee4595 | ||
|
|
2e0df5c82b | ||
|
|
677a5503e2 | ||
|
|
566195129f | ||
|
|
b872340c5d | ||
|
|
1c4fba6b2e | ||
|
|
2adb552618 | ||
|
|
59e27bc41d | ||
|
|
5d1234e2d6 | ||
|
|
1f74f9c721 | ||
|
|
fb2dcb04ae | ||
|
|
abdaa38389 | ||
|
|
0830678526 | ||
|
|
cb827c7ab5 | ||
|
|
e7ae287f38 | ||
|
|
ffb5322ecb | ||
|
|
1ec78cf6a2 | ||
|
|
681bd7abcf | ||
|
|
1a906aac74 | ||
|
|
57553fbbb3 | ||
|
|
738d949f1f | ||
|
|
b362408704 | ||
|
|
32238ba07c | ||
|
|
2172f8b9eb | ||
|
|
609b4914bd | ||
|
|
e23e507a68 | ||
|
|
f69ee6364d | ||
|
|
a468f9301d | ||
|
|
efa541d68d | ||
|
|
068ad1b06a | ||
|
|
f3bec05e1f | ||
|
|
a84dc1b01e | ||
|
|
ca4a3ee972 | ||
|
|
1c817426b8 | ||
|
|
5ec9bc14a9 | ||
|
|
67f451069a | ||
|
|
4eb73af250 | ||
|
|
ef60e23a53 | ||
|
|
2b57d0a9e6 | ||
|
|
ae183ae3ad | ||
|
|
725a3a5106 | ||
|
|
289f573b00 | ||
|
|
a8a6e658e2 | ||
|
|
797bb6fc98 | ||
|
|
41f61503fb | ||
|
|
764c979560 | ||
|
|
a0531ff1f3 | ||
|
|
30d5a4cdc2 | ||
|
|
750ff1a09a | ||
|
|
c5cc247e2d | ||
|
|
813bac5e52 | ||
|
|
267cf1597e | ||
|
|
4716312c3f | ||
|
|
31b735c3d3 | ||
|
|
e9baae4d03 | ||
|
|
f97142200f | ||
|
|
34bc25b023 | ||
|
|
ede24b9fd5 | ||
|
|
c656b61ce2 | ||
|
|
3fb804fbd1 | ||
|
|
eb5f126c76 | ||
|
|
815d0732c3 | ||
|
|
a517cf216d | ||
|
|
e5a6eede24 | ||
|
|
7a3c28f3f6 | ||
|
|
49381cea84 | ||
|
|
9afa92a22d | ||
|
|
6719903012 | ||
|
|
2f0047e6c0 | ||
|
|
71a708b515 | ||
|
|
50b9011323 | ||
|
|
e4bd4e2371 | ||
|
|
f662dc29ed | ||
|
|
e4ffd5c6ab | ||
|
|
588bc9e691 | ||
|
|
3e98887954 | ||
|
|
78319f65eb | ||
|
|
c467fd39c1 | ||
|
|
896bd82f97 | ||
|
|
3a87d7536b | ||
|
|
1b245a568f | ||
|
|
95eb6b2c63 | ||
|
|
616d5d2cdb | ||
|
|
9e1edc58ab | ||
|
|
7df227b584 | ||
|
|
be0f2ab537 | ||
|
|
885c679033 | ||
|
|
0575e714ae | ||
|
|
d1f3074c90 | ||
|
|
3f3a3b161b | ||
|
|
c5af29ddde | ||
|
|
40dd6100ab | ||
|
|
42cd1a85d9 | ||
|
|
c196b46337 | ||
|
|
e5e133475b | ||
|
|
67d191bbf3 | ||
|
|
c9502f8462 | ||
|
|
3dbc3b1c5a | ||
|
|
c54928bb64 | ||
|
|
d40d7f0566 | ||
|
|
b73ddab2e2 | ||
|
|
069523adcd | ||
|
|
fc310ee24c | ||
|
|
b53d9a0f6f | ||
|
|
035ff7dfe7 | ||
|
|
2bbf197108 | ||
|
|
5d2665207a | ||
|
|
8618b01800 | ||
|
|
d59d51ae33 | ||
|
|
a313a19cec | ||
|
|
c5f7565ac0 | ||
|
|
f52486528b | ||
|
|
d17399d98b | ||
|
|
1eadaad9bd | ||
|
|
22e424a78c | ||
|
|
712b57e46b | ||
|
|
bc914f0a09 | ||
|
|
1da2ccbb1c | ||
|
|
c58d802c30 | ||
|
|
8cdd5c69e6 | ||
|
|
2b61e80336 | ||
|
|
d831c47fcd | ||
|
|
53955cb9d2 | ||
|
|
2d9c68c10a | ||
|
|
c987db7941 | ||
|
|
b1d4eb5c8c | ||
|
|
c929efbf24 | ||
|
|
0c3f379fb3 | ||
|
|
2d79ae9e7a | ||
|
|
d0ac9b3a94 | ||
|
|
719824b518 | ||
|
|
f6f290878a | ||
|
|
4ee9bfc450 | ||
|
|
d3f45fc33d | ||
|
|
ee4cefc813 | ||
|
|
4b7f570ba5 | ||
|
|
6981018003 | ||
|
|
380f27a0bc | ||
|
|
d489bebb85 | ||
|
|
2df1c439c4 | ||
|
|
83ac485f19 | ||
|
|
1c6bf2e828 | ||
|
|
bfdab76d71 | ||
|
|
90dba00c07 | ||
|
|
1d38521819 | ||
|
|
bc8b56018d | ||
|
|
073b22de40 | ||
|
|
8bc8efe6c0 | ||
|
|
fe80d3029b | ||
|
|
86410d4407 | ||
|
|
aba19cf986 | ||
|
|
0a2ca64bf7 | ||
|
|
4761393e88 | ||
|
|
298026491a | ||
|
|
2fe2c47289 | ||
|
|
2d968c6c14 | ||
|
|
0708a4a1e2 | ||
|
|
d2c234db1f | ||
|
|
d2b7ffba3b | ||
|
|
ff2a2a728d | ||
|
|
2b43cec8fe | ||
|
|
6359014017 | ||
|
|
d305c3d6e5 | ||
|
|
9b5c5f39a4 | ||
|
|
627f88508a | ||
|
|
6e6f2c5675 | ||
|
|
7d5aaed8fb | ||
|
|
eac98c4846 | ||
|
|
f048513338 | ||
|
|
4780031b87 | ||
|
|
f54d30ff42 | ||
|
|
17e97afb97 | ||
|
|
d79db74385 | ||
|
|
c412a47139 | ||
|
|
2387f2462e | ||
|
|
6bd2e088fc | ||
|
|
b4874a05c6 | ||
|
|
a77586ec95 | ||
|
|
ed9efe8d72 | ||
|
|
f7431b0b10 | ||
|
|
a340de9da8 | ||
|
|
bf11cf8a7b | ||
|
|
b4dc8b493a | ||
|
|
ce1cf6f4b1 | ||
|
|
8d6422e8b4 | ||
|
|
b3c28266cb | ||
|
|
074f2e343f | ||
|
|
8f6f7334a0 | ||
|
|
b467c22baa | ||
|
|
15d3dd8c74 | ||
|
|
2010f3e455 | ||
|
|
cc95917d53 | ||
|
|
f918fbb504 | ||
|
|
6d4d644900 | ||
|
|
a046af7b8d | ||
|
|
bde00df3ad | ||
|
|
d1080bbb42 | ||
|
|
875d9be7e5 | ||
|
|
e683b3b9ad | ||
|
|
199e098704 | ||
|
|
b4cdfa3011 | ||
|
|
92bfeb96e2 | ||
|
|
f4c9c9de1a | ||
|
|
e60293a0ce | ||
|
|
5659818313 | ||
|
|
1de5bfd809 | ||
|
|
0fa3b25f12 | ||
|
|
02f505d962 | ||
|
|
abdb303069 | ||
|
|
c1e5cd5d59 | ||
|
|
7ea0dda562 | ||
|
|
947e63fc61 | ||
|
|
882e561416 | ||
|
|
1e531a7259 | ||
|
|
2967ae167b | ||
|
|
089840438d | ||
|
|
bcd9ae047e | ||
|
|
20de66a3be | ||
|
|
7587f5f07a | ||
|
|
4ad450f0ba | ||
|
|
2883f42a19 | ||
|
|
d788efbaf6 | ||
|
|
ca0bd87357 | ||
|
|
e246d0dfa0 | ||
|
|
f93c319427 | ||
|
|
eb591e3b4a | ||
|
|
37d7f14148 | ||
|
|
86b1814698 | ||
|
|
d15fae3674 | ||
|
|
6d3a6168d2 | ||
|
|
0467dc1da3 | ||
|
|
d81e8d2de0 | ||
|
|
ec59e2e556 | ||
|
|
cfdb8c06f3 | ||
|
|
32bf5cd0a7 | ||
|
|
a0401a3962 | ||
|
|
4def44ed1e | ||
|
|
bd472b68a1 | ||
|
|
142bac7275 | ||
|
|
80d4ab1dea | ||
|
|
481c995481 | ||
|
|
76e3059b34 | ||
|
|
9c6157563e | ||
|
|
c362900290 | ||
|
|
1573707d39 | ||
|
|
acce592f02 | ||
|
|
24cc03e4ed | ||
|
|
aec1f45023 | ||
|
|
e208a3cce0 | ||
|
|
2f275e19bd | ||
|
|
ac8f12f438 | ||
|
|
c14580965f | ||
|
|
436a5ab3f9 | ||
|
|
3e1fde4515 | ||
|
|
9beec69e85 | ||
|
|
a1116a87df | ||
|
|
cd4bc01d66 | ||
|
|
7e30601246 | ||
|
|
708b3792c9 | ||
|
|
3c52408579 | ||
|
|
ea5f729d58 | ||
|
|
4d3f7f0b5a | ||
|
|
078f6eabdd | ||
|
|
6d6da98e8c | ||
|
|
49f1910425 | ||
|
|
4ba034f988 | ||
|
|
e93ea74def | ||
|
|
5dadbe9090 | ||
|
|
e3c1a8c5fb | ||
|
|
8e50e9553a | ||
|
|
ed47c8268b | ||
|
|
e0cdeaf09e | ||
|
|
fc198d6eae | ||
|
|
977816baa9 | ||
|
|
758c0a0385 | ||
|
|
e4d7e9dda7 | ||
|
|
baadfee9e0 | ||
|
|
25620603b5 | ||
|
|
a5e306ccae | ||
|
|
e09801442e | ||
|
|
08fb3ccb76 | ||
|
|
e9887cf89e | ||
|
|
de33bd057f | ||
|
|
e04d37a694 | ||
|
|
6e7a1ea09b | ||
|
|
08b2c3186d | ||
|
|
6ca3989635 | ||
|
|
c3019bed16 | ||
|
|
7f5615d109 | ||
|
|
43c76dc1af | ||
|
|
89deff7e06 | ||
|
|
d536f7f56c | ||
|
|
8f43575a19 | ||
|
|
53adc67be1 | ||
|
|
e4addcadc8 | ||
|
|
c48c1e6acd | ||
|
|
cd2baa25c8 | ||
|
|
93e19d0f82 | ||
|
|
a8527dee70 | ||
|
|
b209be1618 | ||
|
|
fa55d9f402 | ||
|
|
c1aeb0b0a1 | ||
|
|
6e2bd99600 | ||
|
|
a99c1f93e1 | ||
|
|
abc2e2be69 | ||
|
|
7aff1f857b | ||
|
|
e47a94aeaa | ||
|
|
12730e6cb5 | ||
|
|
9909a211a4 | ||
|
|
32591514de | ||
|
|
05f65e0d9d | ||
|
|
353b032efd | ||
|
|
fb16a9e50c | ||
|
|
32165af41d | ||
|
|
3860d8d2fc | ||
|
|
dd6a8cdb80 | ||
|
|
27562e52ef | ||
|
|
693c9d67dc | ||
|
|
71cd68d38f | ||
|
|
624283cee7 | ||
|
|
0bf9d2f040 | ||
|
|
c01d893b6b | ||
|
|
3b9d5c7ab6 | ||
|
|
05d5d90e3e | ||
|
|
53b84a9271 | ||
|
|
44688a4ce8 | ||
|
|
908f7f5ccb | ||
|
|
79c3ce14e8 | ||
|
|
d8d0b1deb8 | ||
|
|
22b9f92663 | ||
|
|
0900480e62 | ||
|
|
6b41f91543 | ||
|
|
708de736e0 | ||
|
|
816b23e277 | ||
|
|
b1e472879f | ||
|
|
714f734e58 | ||
|
|
4f279f1068 | ||
|
|
e2194894ce | ||
|
|
6bfc7d5c7f | ||
|
|
0018da8f8e | ||
|
|
a4ddac5039 | ||
|
|
839bc06f88 | ||
|
|
2a1228c949 | ||
|
|
921d56a13d | ||
|
|
9fa1549e09 | ||
|
|
e0d19e256f | ||
|
|
18e8bf56e6 | ||
|
|
83500b7af5 | ||
|
|
548e8af27b | ||
|
|
54afced00a | ||
|
|
b89967ecac | ||
|
|
130a25deaa | ||
|
|
bc52dad3f5 | ||
|
|
4ce75e3ab0 | ||
|
|
a681b31630 | ||
|
|
9777e87b0f | ||
|
|
8647201b1e | ||
|
|
543d3f2f85 | ||
|
|
0cd02d8de8 | ||
|
|
e93a486a85 | ||
|
|
2225903719 | ||
|
|
a854ba3ddf | ||
|
|
bededf4bec | ||
|
|
2fcb35e46c | ||
|
|
120509a02e | ||
|
|
6aca3ea0cc | ||
|
|
93ab09f6c5 | ||
|
|
c9fe2eb1d5 | ||
|
|
5d4666d538 | ||
|
|
44549e2b2a | ||
|
|
b20f8adb5f | ||
|
|
0983ef309f | ||
|
|
8a24c037ed | ||
|
|
dad1d7fda9 | ||
|
|
cce7c1c7c8 | ||
|
|
ac209cfefb | ||
|
|
62e9058966 | ||
|
|
21a202e47d | ||
|
|
274c63c166 | ||
|
|
207d10c2be | ||
|
|
9dc220e0d5 | ||
|
|
73f0cf23f8 | ||
|
|
0b24380b2e | ||
|
|
ff485845bf | ||
|
|
198e408ba4 | ||
|
|
2f5f4661c2 | ||
|
|
430dad82db | ||
|
|
9fce02c2ef | ||
|
|
9d2d7e33e3 | ||
|
|
fc390e0b4c | ||
|
|
2d3fbf3080 | ||
|
|
b7152bfb0a | ||
|
|
b3f216d25d | ||
|
|
1caa12c7e8 | ||
|
|
94e3a9a5f8 | ||
|
|
9cf20708e6 | ||
|
|
c60b7b0252 | ||
|
|
3477bcefc3 | ||
|
|
549a76ee7e | ||
|
|
6c32cebe83 | ||
|
|
d30d4d202a | ||
|
|
d545622d0c | ||
|
|
f33016d523 | ||
|
|
8a531262e7 | ||
|
|
cf0dd510c2 | ||
|
|
d735d1bda8 | ||
|
|
e4dc83fc86 | ||
|
|
13263a8d1e | ||
|
|
3e06b5f8c8 | ||
|
|
857fc6a3c6 | ||
|
|
ad461e5b8c | ||
|
|
5464ef4353 | ||
|
|
e8f4737e81 | ||
|
|
1dcaf98774 | ||
|
|
e081267a9b | ||
|
|
6ec7f4609a | ||
|
|
0bef5ccfa9 | ||
|
|
512545b657 | ||
|
|
390d9ac57c | ||
|
|
10abfb847f | ||
|
|
a19611c3ae | ||
|
|
44840aa0ff | ||
|
|
f0945efd3a | ||
|
|
cba29bb2c5 | ||
|
|
08c9636cd9 | ||
|
|
d940c4ac82 | ||
|
|
208e844c97 | ||
|
|
9f808a42a3 | ||
|
|
cf9a578815 | ||
|
|
449f3556a7 | ||
|
|
a3736d6cd6 | ||
|
|
f975af721e | ||
|
|
e23dfe7846 | ||
|
|
255a72e2ae | ||
|
|
de3aa36b1f | ||
|
|
ea844f6501 | ||
|
|
9bbdc5f8d9 | ||
|
|
0644e634e7 | ||
|
|
66774631e7 | ||
|
|
735982ce4e | ||
|
|
0c207b4e2f | ||
|
|
cf5d7fd192 | ||
|
|
a19aadd826 | ||
|
|
f62da0e273 | ||
|
|
af6f9466a8 | ||
|
|
2ee53d8318 | ||
|
|
b6a5989770 | ||
|
|
193811cb9b | ||
|
|
0c23177455 | ||
|
|
c525ca5642 | ||
|
|
e4a726672d | ||
|
|
a897615d0e | ||
|
|
3e5ffe10b7 | ||
|
|
13131c769c | ||
|
|
449050b6df | ||
|
|
01b5251a39 | ||
|
|
6b6c976061 | ||
|
|
eeed61a9f6 | ||
|
|
4afe7654af | ||
|
|
acdd504a7c | ||
|
|
65c9477bb9 | ||
|
|
eaa11c76ab | ||
|
|
2ba0eb77ad | ||
|
|
3761e07c6d | ||
|
|
c787c8ece7 | ||
|
|
f8e8e2c8a4 | ||
|
|
3960ec16b4 | ||
|
|
ddb04b97a8 | ||
|
|
868d593c5c | ||
|
|
e6a5524cbc | ||
|
|
cf179878ae | ||
|
|
394f10f384 | ||
|
|
df814b1acb | ||
|
|
b7439693fc | ||
|
|
c009fc8dc8 | ||
|
|
cf62c0802f | ||
|
|
5f19372429 | ||
|
|
adf2eccc9c | ||
|
|
ce463bf780 | ||
|
|
31fe442476 | ||
|
|
45daa46cf9 | ||
|
|
83c12f5636 | ||
|
|
1683729772 | ||
|
|
21cd3d8dbb | ||
|
|
4b34905358 | ||
|
|
c0214f16fc | ||
|
|
458cb95422 | ||
|
|
ef844c168c | ||
|
|
96e5924c0c | ||
|
|
d20fa79897 | ||
|
|
f1a49cdff0 | ||
|
|
baa1339573 | ||
|
|
53802829b6 | ||
|
|
ee7866590f | ||
|
|
94cb4e4e0a | ||
|
|
78e5ab2cbb | ||
|
|
a52bf7774c | ||
|
|
4aed278f9c | ||
|
|
1631e1687b | ||
|
|
4738171fd3 | ||
|
|
001eabe25d | ||
|
|
3d535c4c72 | ||
|
|
f59a3ca233 | ||
|
|
d3134afc5a | ||
|
|
39bf821f78 | ||
|
|
5324dad0c6 | ||
|
|
ab92bbf3d3 | ||
|
|
63a481629e | ||
|
|
d44d075b20 | ||
|
|
030d71c52a | ||
|
|
2fe0a2635e | ||
|
|
ae45f474e3 | ||
|
|
33e98a995e | ||
|
|
92b0e4bfb6 | ||
|
|
2403ac17dc | ||
|
|
ca74047828 | ||
|
|
21e951092d | ||
|
|
3ba97b49ba | ||
|
|
5e739e483e | ||
|
|
992a4563c2 | ||
|
|
3e9e1cb6ed | ||
|
|
61f876480a | ||
|
|
c2b1d1356c | ||
|
|
d5ac569a6a | ||
|
|
0b1ddd39d5 | ||
|
|
cbe0502154 | ||
|
|
816914dd10 | ||
|
|
bcb4320f5d | ||
|
|
bc4d5d37ff | ||
|
|
1996763751 | ||
|
|
99a0a5cc48 | ||
|
|
63c91d2d19 | ||
|
|
d6be6cf435 | ||
|
|
8f7f9a27ee | ||
|
|
b776d4d882 | ||
|
|
63c81c2403 | ||
|
|
ad9a77ff19 | ||
|
|
2e17a15d60 | ||
|
|
3fae20673b | ||
|
|
5b36c1c286 | ||
|
|
4fc4d011f2 | ||
|
|
91aa9754bb | ||
|
|
a6ba407ebf | ||
|
|
1cdb128c6b | ||
|
|
38ca6b7642 | ||
|
|
65c7a60c92 | ||
|
|
77c802ee5f | ||
|
|
a8f23159f4 | ||
|
|
ee8a7a6f61 | ||
|
|
4a612b9436 | ||
|
|
4d89995016 | ||
|
|
368c453504 | ||
|
|
1694851395 | ||
|
|
1ed4f3f456 | ||
|
|
588d49eeb9 | ||
|
|
22d4f22da6 | ||
|
|
ced640f024 | ||
|
|
7b8804b53c | ||
|
|
5d25742faf | ||
|
|
4460b18c5e | ||
|
|
cb5eb499d9 | ||
|
|
9382c6d390 | ||
|
|
7cd091cfa2 | ||
|
|
961dc4cf5d | ||
|
|
f77193dc48 | ||
|
|
a4b31d2cdc | ||
|
|
83b3edae86 | ||
|
|
ebc4601ed4 | ||
|
|
a478c02967 | ||
|
|
aac1f81704 | ||
|
|
100b85abe0 | ||
|
|
bb52122db1 | ||
|
|
397b36c557 | ||
|
|
77b041ea67 | ||
|
|
b1d896ae70 | ||
|
|
8f5157d8dd | ||
|
|
c5cf5f70c8 | ||
|
|
88f7856c9b | ||
|
|
d843d1d5b1 | ||
|
|
921b416374 | ||
|
|
c2eac39145 | ||
|
|
467dbf723d | ||
|
|
9460b476dd | ||
|
|
16fb5a7d98 | ||
|
|
c17da5951d | ||
|
|
66a959d21c | ||
|
|
480037b84b | ||
|
|
fa7867ab18 | ||
|
|
0c5cdfae0a | ||
|
|
7e5f82d65b | ||
|
|
9753b36769 | ||
|
|
fd2c1e8c24 | ||
|
|
e5c9defac8 | ||
|
|
33c8c0f0d6 | ||
|
|
197b41d96f | ||
|
|
52a8bcbbfe | ||
|
|
e102b6c565 | ||
|
|
f899508253 | ||
|
|
ab20d5fccf | ||
|
|
3c9827b1e1 | ||
|
|
9636fa1da1 | ||
|
|
b5ad577821 | ||
|
|
ca483a91f3 | ||
|
|
49e904c25a | ||
|
|
9a0c0bfff5 | ||
|
|
2ed1214d37 | ||
|
|
adf417e2f1 | ||
|
|
ab333dc0b3 | ||
|
|
3f079a8fea | ||
|
|
90db25e4de | ||
|
|
ec793e22a1 | ||
|
|
c76f6caa2b | ||
|
|
60245d5399 | ||
|
|
8fb2ec61b4 | ||
|
|
4a939ad607 | ||
|
|
ba1be3df10 | ||
|
|
93333f0ece | ||
|
|
dcde5d9d54 | ||
|
|
52af5c85f0 | ||
|
|
d47f17c7ad | ||
|
|
8533659b74 | ||
|
|
2f658a3d4e | ||
|
|
2c31b5f11f | ||
|
|
c66bd6259c | ||
|
|
145c64389d | ||
|
|
23263e634e | ||
|
|
2074865a7c | ||
|
|
4cb9f54204 | ||
|
|
78b1d7de0f | ||
|
|
c296790226 | ||
|
|
d7fec15597 | ||
|
|
f33fda8313 | ||
|
|
62ce58e148 | ||
|
|
db70633721 | ||
|
|
8c62b01d19 | ||
|
|
5f3b2c9a0d | ||
|
|
1afac53318 | ||
|
|
78d2ff1645 | ||
|
|
31082e0184 | ||
|
|
7e942d6481 | ||
|
|
af0659595e | ||
|
|
89962bd393 | ||
|
|
c71fe556a3 | ||
|
|
4c88e098c6 | ||
|
|
8069c19eef | ||
|
|
91fd6a1014 |
15
.github/renovate.json5
vendored
15
.github/renovate.json5
vendored
@@ -1,7 +1,8 @@
|
||||
{
|
||||
"$schema": "https://docs.renovatebot.com/renovate-schema.json",
|
||||
|
||||
"extends": [
|
||||
"config:base",
|
||||
"config:best-practices",
|
||||
":assignee(JustArchi)",
|
||||
":automergeBranch",
|
||||
":automergeDigest",
|
||||
@@ -10,7 +11,17 @@
|
||||
":disableRateLimiting",
|
||||
":label(🤖 Automatic)"
|
||||
],
|
||||
|
||||
"git-submodules": {
|
||||
"enabled": true
|
||||
}
|
||||
},
|
||||
|
||||
"packageRules": [
|
||||
{
|
||||
// TODO: Allow updates of selected packages with no stable release (yet) to latest versions
|
||||
"matchManagers": [ "nuget" ],
|
||||
"matchPackageNames": [ "Microsoft.CodeAnalysis.ResxSourceGenerator", "OpenTelemetry.Exporter.Prometheus.AspNetCore" ],
|
||||
"ignoreUnstable": false
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
10
.github/workflows/ci.yml
vendored
10
.github/workflows/ci.yml
vendored
@@ -5,7 +5,7 @@ on: [push, pull_request]
|
||||
env:
|
||||
DOTNET_CLI_TELEMETRY_OPTOUT: true
|
||||
DOTNET_NOLOGO: true
|
||||
DOTNET_SDK_VERSION: 9.0
|
||||
DOTNET_SDK_VERSION: 10.0
|
||||
|
||||
permissions: {}
|
||||
|
||||
@@ -15,19 +15,19 @@ jobs:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
configuration: [Debug, Release]
|
||||
os: [macos-latest, ubuntu-latest, windows-latest]
|
||||
os: [macos-latest, macos-15-intel, ubuntu-latest, ubuntu-24.04-arm, windows-latest, windows-11-arm]
|
||||
|
||||
runs-on: ${{ matrix.os }}
|
||||
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v4.2.2
|
||||
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
|
||||
with:
|
||||
show-progress: false
|
||||
submodules: recursive
|
||||
|
||||
- name: Setup .NET Core
|
||||
uses: actions/setup-dotnet@v4.3.0
|
||||
uses: actions/setup-dotnet@2016bd2012dba4e32de620c46fe006a3ac9f0602 # v5.0.1
|
||||
with:
|
||||
dotnet-version: ${{ env.DOTNET_SDK_VERSION }}
|
||||
|
||||
@@ -38,4 +38,4 @@ jobs:
|
||||
run: dotnet build -c "${{ matrix.configuration }}" -p:ContinuousIntegrationBuild=true -p:UseAppHost=false --nologo
|
||||
|
||||
- name: Run ${{ matrix.configuration }} ArchiSteamFarm.Tests
|
||||
run: dotnet test ArchiSteamFarm.Tests -c "${{ matrix.configuration }}" -p:ContinuousIntegrationBuild=true -p:UseAppHost=false --nologo
|
||||
run: dotnet test ArchiSteamFarm.Tests -c "${{ matrix.configuration }}" -p:ContinuousIntegrationBuild=true -p:UseAppHost=false --filter TestCategory!=Manual --nologo
|
||||
|
||||
39
.github/workflows/code-quality.yml
vendored
39
.github/workflows/code-quality.yml
vendored
@@ -1,39 +0,0 @@
|
||||
name: ASF-code-quality
|
||||
|
||||
on:
|
||||
- push
|
||||
|
||||
env:
|
||||
DOTNET_CLI_TELEMETRY_OPTOUT: true
|
||||
DOTNET_NOLOGO: true
|
||||
|
||||
permissions:
|
||||
checks: write
|
||||
contents: write
|
||||
pull-requests: write
|
||||
security-events: write
|
||||
|
||||
jobs:
|
||||
main:
|
||||
environment: qa-qodana
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v4.2.2
|
||||
with:
|
||||
show-progress: false
|
||||
|
||||
- name: Run Qodana scan
|
||||
uses: JetBrains/qodana-action@v2024.3.4
|
||||
with:
|
||||
args: --config,.github/qodana.yaml,--property=idea.headless.enable.statistics=false
|
||||
pr-mode: false
|
||||
upload-result: true
|
||||
env:
|
||||
QODANA_TOKEN: ${{ secrets.QODANA_TOKEN }}
|
||||
|
||||
- name: Report Qodana results to GitHub
|
||||
uses: github/codeql-action/upload-sarif@v3.28.8
|
||||
with:
|
||||
sarif_file: ${{ runner.temp }}/qodana/results/qodana.sarif.json
|
||||
4
.github/workflows/crowdin-ci.yml
vendored
4
.github/workflows/crowdin-ci.yml
vendored
@@ -14,13 +14,13 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v4.2.2
|
||||
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
|
||||
with:
|
||||
show-progress: false
|
||||
submodules: recursive
|
||||
|
||||
- name: Upload latest strings for translation on Crowdin
|
||||
uses: crowdin/github-action@v2.5.2
|
||||
uses: crowdin/github-action@60debf382ee245b21794321190ad0501db89d8c1 # v2.13.0
|
||||
with:
|
||||
crowdin_branch_name: main
|
||||
config: '.github/crowdin.yml'
|
||||
|
||||
6
.github/workflows/docker-ci.yml
vendored
6
.github/workflows/docker-ci.yml
vendored
@@ -19,16 +19,16 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v4.2.2
|
||||
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
|
||||
with:
|
||||
show-progress: false
|
||||
submodules: recursive
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v3.8.0
|
||||
uses: docker/setup-buildx-action@e468171a9de216ec08956ac3ada2f0791b6bd435 # v3.11.1
|
||||
|
||||
- name: Build ${{ matrix.configuration }} Docker image from ${{ matrix.file }}
|
||||
uses: docker/build-push-action@v6.13.0
|
||||
uses: docker/build-push-action@263435318d21b8e681c14492fe198d362a7d2c83 # v6.18.0
|
||||
with:
|
||||
build-args: CONFIGURATION=${{ matrix.configuration }}
|
||||
context: .
|
||||
|
||||
10
.github/workflows/docker-publish-latest.yml
vendored
10
.github/workflows/docker-publish-latest.yml
vendored
@@ -18,23 +18,23 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v4.2.2
|
||||
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
|
||||
with:
|
||||
show-progress: false
|
||||
submodules: recursive
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v3.8.0
|
||||
uses: docker/setup-buildx-action@e468171a9de216ec08956ac3ada2f0791b6bd435 # v3.11.1
|
||||
|
||||
- name: Login to ghcr.io
|
||||
uses: docker/login-action@v3.3.0
|
||||
uses: docker/login-action@5e57cd118135c172c3672efd75eb46360885c0ef # v3.6.0
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ github.actor }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Login to DockerHub
|
||||
uses: docker/login-action@v3.3.0
|
||||
uses: docker/login-action@5e57cd118135c172c3672efd75eb46360885c0ef # v3.6.0
|
||||
with:
|
||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||
@@ -50,7 +50,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@v6.13.0
|
||||
uses: docker/build-push-action@263435318d21b8e681c14492fe198d362a7d2c83 # v6.18.0
|
||||
with:
|
||||
context: .
|
||||
file: Dockerfile.Service
|
||||
|
||||
12
.github/workflows/docker-publish-main.yml
vendored
12
.github/workflows/docker-publish-main.yml
vendored
@@ -19,23 +19,23 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v4.2.2
|
||||
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
|
||||
with:
|
||||
show-progress: false
|
||||
submodules: recursive
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v3.8.0
|
||||
uses: docker/setup-buildx-action@e468171a9de216ec08956ac3ada2f0791b6bd435 # v3.11.1
|
||||
|
||||
- name: Login to ghcr.io
|
||||
uses: docker/login-action@v3.3.0
|
||||
uses: docker/login-action@5e57cd118135c172c3672efd75eb46360885c0ef # v3.6.0
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ github.actor }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Login to DockerHub
|
||||
uses: docker/login-action@v3.3.0
|
||||
uses: docker/login-action@5e57cd118135c172c3672efd75eb46360885c0ef # v3.6.0
|
||||
with:
|
||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||
@@ -50,7 +50,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@v6.13.0
|
||||
uses: docker/build-push-action@263435318d21b8e681c14492fe198d362a7d2c83 # v6.18.0
|
||||
with:
|
||||
context: .
|
||||
platforms: ${{ env.PLATFORMS }}
|
||||
@@ -69,7 +69,7 @@ jobs:
|
||||
push: true
|
||||
|
||||
- name: Update DockerHub repository description
|
||||
uses: peter-evans/dockerhub-description@v4.0.0
|
||||
uses: peter-evans/dockerhub-description@1b9a80c056b620d92cedb9d9b5a223409c68ddfa # v5.0.0
|
||||
with:
|
||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||
|
||||
10
.github/workflows/docker-publish-released.yml
vendored
10
.github/workflows/docker-publish-released.yml
vendored
@@ -19,23 +19,23 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v4.2.2
|
||||
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
|
||||
with:
|
||||
show-progress: false
|
||||
submodules: recursive
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v3.8.0
|
||||
uses: docker/setup-buildx-action@e468171a9de216ec08956ac3ada2f0791b6bd435 # v3.11.1
|
||||
|
||||
- name: Login to ghcr.io
|
||||
uses: docker/login-action@v3.3.0
|
||||
uses: docker/login-action@5e57cd118135c172c3672efd75eb46360885c0ef # v3.6.0
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ github.actor }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Login to DockerHub
|
||||
uses: docker/login-action@v3.3.0
|
||||
uses: docker/login-action@5e57cd118135c172c3672efd75eb46360885c0ef # v3.6.0
|
||||
with:
|
||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||
@@ -51,7 +51,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@v6.13.0
|
||||
uses: docker/build-push-action@263435318d21b8e681c14492fe198d362a7d2c83 # v6.18.0
|
||||
with:
|
||||
context: .
|
||||
platforms: ${{ env.PLATFORMS }}
|
||||
|
||||
2
.github/workflows/lock-threads.yml
vendored
2
.github/workflows/lock-threads.yml
vendored
@@ -18,7 +18,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Lock inactive threads
|
||||
uses: dessant/lock-threads@v5.0.1
|
||||
uses: dessant/lock-threads@1bf7ec25051fe7c00bdd17e6a7cf3d7bfb7dc771 # v5.0.1
|
||||
with:
|
||||
discussion-inactive-days: 90
|
||||
issue-inactive-days: 60
|
||||
|
||||
61
.github/workflows/publish.yml
vendored
61
.github/workflows/publish.yml
vendored
@@ -6,7 +6,7 @@ env:
|
||||
CONFIGURATION: Release
|
||||
DOTNET_CLI_TELEMETRY_OPTOUT: true
|
||||
DOTNET_NOLOGO: true
|
||||
DOTNET_SDK_VERSION: 9.0
|
||||
DOTNET_SDK_VERSION: 10.0
|
||||
NODE_JS_VERSION: 'lts/*'
|
||||
PLUGINS_BUNDLED: ArchiSteamFarm.OfficialPlugins.ItemsMatcher ArchiSteamFarm.OfficialPlugins.MobileAuthenticator ArchiSteamFarm.OfficialPlugins.SteamTokenDumper
|
||||
PLUGINS_INCLUDED: ArchiSteamFarm.OfficialPlugins.Monitoring # Apart from declaring them here, there is certain amount of hardcoding needed below for uploading
|
||||
@@ -19,13 +19,13 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v4.2.2
|
||||
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
|
||||
with:
|
||||
show-progress: false
|
||||
submodules: recursive
|
||||
|
||||
- name: Setup Node.js with npm
|
||||
uses: actions/setup-node@v4.2.0
|
||||
uses: actions/setup-node@395ad3262231945c25e8478fd5baf05154b1d79f # v6.1.0
|
||||
with:
|
||||
check-latest: true
|
||||
node-version: ${{ env.NODE_JS_VERSION }}
|
||||
@@ -43,8 +43,9 @@ jobs:
|
||||
run: npm run-script deploy --no-progress --prefix ASF-ui
|
||||
|
||||
- name: Upload ASF-ui
|
||||
uses: actions/upload-artifact@v4.6.0
|
||||
uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5.0.0
|
||||
with:
|
||||
compression-level: 1
|
||||
if-no-files-found: error
|
||||
name: ASF-ui
|
||||
path: ASF-ui/dist
|
||||
@@ -73,7 +74,6 @@ jobs:
|
||||
- os: windows-latest
|
||||
variant: win-x64
|
||||
|
||||
environment: build
|
||||
runs-on: ${{ matrix.os }}
|
||||
|
||||
permissions:
|
||||
@@ -82,12 +82,12 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v4.2.2
|
||||
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
|
||||
with:
|
||||
show-progress: false
|
||||
|
||||
- name: Setup .NET Core
|
||||
uses: actions/setup-dotnet@v4.3.0
|
||||
uses: actions/setup-dotnet@2016bd2012dba4e32de620c46fe006a3ac9f0602 # v5.0.1
|
||||
with:
|
||||
dotnet-version: ${{ env.DOTNET_SDK_VERSION }}
|
||||
|
||||
@@ -95,7 +95,7 @@ jobs:
|
||||
run: dotnet --info
|
||||
|
||||
- name: Download previously built ASF-ui
|
||||
uses: actions/download-artifact@v4.1.8
|
||||
uses: actions/download-artifact@018cc2cf5baa6db3ef3c5f8a56943fffe632ef53 # v6.0.0
|
||||
with:
|
||||
name: ASF-ui
|
||||
path: ASF-ui/dist
|
||||
@@ -362,13 +362,14 @@ jobs:
|
||||
|
||||
- name: Generate artifact attestation for ASF-${{ matrix.variant }}.zip
|
||||
if: ${{ github.event_name == 'push' }}
|
||||
uses: actions/attest-build-provenance@v2.2.0
|
||||
uses: actions/attest-build-provenance@977bb373ede98d70efdf65b84cb5f73e068dcc2a # v3.0.0
|
||||
with:
|
||||
subject-path: out/ASF-${{ matrix.variant }}.zip
|
||||
|
||||
- name: Upload ASF-${{ matrix.variant }}
|
||||
uses: actions/upload-artifact@v4.6.0
|
||||
uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5.0.0
|
||||
with:
|
||||
compression-level: 1
|
||||
if-no-files-found: error
|
||||
name: ${{ matrix.os }}_ASF-${{ matrix.variant }}
|
||||
path: out/ASF-${{ matrix.variant }}.zip
|
||||
@@ -409,14 +410,15 @@ jobs:
|
||||
|
||||
- name: Generate artifact attestation for ArchiSteamFarm.OfficialPlugins.Monitoring
|
||||
if: ${{ github.event_name == 'push' && matrix.os == 'ubuntu-latest' && matrix.variant == 'generic' }}
|
||||
uses: actions/attest-build-provenance@v2.2.0
|
||||
uses: actions/attest-build-provenance@977bb373ede98d70efdf65b84cb5f73e068dcc2a # v3.0.0
|
||||
with:
|
||||
subject-path: out/ArchiSteamFarm.OfficialPlugins.Monitoring.zip
|
||||
|
||||
- name: Upload ArchiSteamFarm.OfficialPlugins.Monitoring
|
||||
if: ${{ matrix.os == 'ubuntu-latest' && matrix.variant == 'generic' }}
|
||||
uses: actions/upload-artifact@v4.6.0
|
||||
uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5.0.0
|
||||
with:
|
||||
compression-level: 1
|
||||
if-no-files-found: error
|
||||
name: ArchiSteamFarm.OfficialPlugins.Monitoring
|
||||
path: out/ArchiSteamFarm.OfficialPlugins.Monitoring.zip
|
||||
@@ -434,66 +436,66 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v4.2.2
|
||||
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
|
||||
with:
|
||||
show-progress: false
|
||||
|
||||
- name: Download ASF-generic artifact from ubuntu-latest
|
||||
uses: actions/download-artifact@v4.1.8
|
||||
uses: actions/download-artifact@018cc2cf5baa6db3ef3c5f8a56943fffe632ef53 # v6.0.0
|
||||
with:
|
||||
name: ubuntu-latest_ASF-generic
|
||||
path: out
|
||||
|
||||
- name: Download ASF-linux-arm artifact from ubuntu-latest
|
||||
uses: actions/download-artifact@v4.1.8
|
||||
uses: actions/download-artifact@018cc2cf5baa6db3ef3c5f8a56943fffe632ef53 # v6.0.0
|
||||
with:
|
||||
name: ubuntu-latest_ASF-linux-arm
|
||||
path: out
|
||||
|
||||
- name: Download ASF-linux-arm64 artifact from ubuntu-latest
|
||||
uses: actions/download-artifact@v4.1.8
|
||||
uses: actions/download-artifact@018cc2cf5baa6db3ef3c5f8a56943fffe632ef53 # v6.0.0
|
||||
with:
|
||||
name: ubuntu-latest_ASF-linux-arm64
|
||||
path: out
|
||||
|
||||
- name: Download ASF-linux-x64 artifact from ubuntu-latest
|
||||
uses: actions/download-artifact@v4.1.8
|
||||
uses: actions/download-artifact@018cc2cf5baa6db3ef3c5f8a56943fffe632ef53 # v6.0.0
|
||||
with:
|
||||
name: ubuntu-latest_ASF-linux-x64
|
||||
path: out
|
||||
|
||||
- name: Download ASF-osx-arm64 artifact from macos-latest
|
||||
uses: actions/download-artifact@v4.1.8
|
||||
uses: actions/download-artifact@018cc2cf5baa6db3ef3c5f8a56943fffe632ef53 # v6.0.0
|
||||
with:
|
||||
name: macos-latest_ASF-osx-arm64
|
||||
path: out
|
||||
|
||||
- name: Download ASF-osx-x64 artifact from macos-latest
|
||||
uses: actions/download-artifact@v4.1.8
|
||||
uses: actions/download-artifact@018cc2cf5baa6db3ef3c5f8a56943fffe632ef53 # v6.0.0
|
||||
with:
|
||||
name: macos-latest_ASF-osx-x64
|
||||
path: out
|
||||
|
||||
- name: Download ASF-win-arm64 artifact from windows-latest
|
||||
uses: actions/download-artifact@v4.1.8
|
||||
uses: actions/download-artifact@018cc2cf5baa6db3ef3c5f8a56943fffe632ef53 # v6.0.0
|
||||
with:
|
||||
name: windows-latest_ASF-win-arm64
|
||||
path: out
|
||||
|
||||
- name: Download ASF-win-x64 artifact from windows-latest
|
||||
uses: actions/download-artifact@v4.1.8
|
||||
uses: actions/download-artifact@018cc2cf5baa6db3ef3c5f8a56943fffe632ef53 # v6.0.0
|
||||
with:
|
||||
name: windows-latest_ASF-win-x64
|
||||
path: out
|
||||
|
||||
- name: Download ArchiSteamFarm.OfficialPlugins.Monitoring artifact
|
||||
uses: actions/download-artifact@v4.1.8
|
||||
uses: actions/download-artifact@018cc2cf5baa6db3ef3c5f8a56943fffe632ef53 # v6.0.0
|
||||
with:
|
||||
name: ArchiSteamFarm.OfficialPlugins.Monitoring
|
||||
path: out
|
||||
|
||||
- name: Import GPG key for signing
|
||||
uses: crazy-max/ghaction-import-gpg@v6.2.0
|
||||
uses: crazy-max/ghaction-import-gpg@e89d40939c28e39f97cf32126055eeae86ba74ec # v6.3.0
|
||||
with:
|
||||
gpg_private_key: ${{ secrets.ARCHIBOT_GPG_PRIVATE_KEY }}
|
||||
|
||||
@@ -507,36 +509,39 @@ jobs:
|
||||
gpg -a -b -o SHA512SUMS.sign SHA512SUMS
|
||||
|
||||
- name: Generate artifact attestation for SHA512SUMS
|
||||
uses: actions/attest-build-provenance@v2.2.0
|
||||
uses: actions/attest-build-provenance@977bb373ede98d70efdf65b84cb5f73e068dcc2a # v3.0.0
|
||||
with:
|
||||
subject-path: out/SHA512SUMS
|
||||
|
||||
- name: Upload SHA512SUMS
|
||||
uses: actions/upload-artifact@v4.6.0
|
||||
uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5.0.0
|
||||
with:
|
||||
compression-level: 1
|
||||
if-no-files-found: error
|
||||
name: SHA512SUMS
|
||||
path: out/SHA512SUMS
|
||||
|
||||
- name: Generate artifact attestation for SHA512SUMS.sign
|
||||
uses: actions/attest-build-provenance@v2.2.0
|
||||
uses: actions/attest-build-provenance@977bb373ede98d70efdf65b84cb5f73e068dcc2a # v3.0.0
|
||||
with:
|
||||
subject-path: out/SHA512SUMS.sign
|
||||
|
||||
- name: Upload SHA512SUMS.sign
|
||||
uses: actions/upload-artifact@v4.6.0
|
||||
uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5.0.0
|
||||
with:
|
||||
compression-level: 1
|
||||
if-no-files-found: error
|
||||
name: SHA512SUMS.sign
|
||||
path: out/SHA512SUMS.sign
|
||||
|
||||
- name: Create ArchiSteamFarm GitHub release
|
||||
uses: ncipollo/release-action@v1.15.0
|
||||
uses: ncipollo/release-action@b7eabc95ff50cbeeedec83973935c8f306dfcd0b # v1.20.0
|
||||
with:
|
||||
allowUpdates: true
|
||||
artifactErrorsFailBuild: true
|
||||
artifacts: "out/*"
|
||||
bodyFile: .github/RELEASE_TEMPLATE.md
|
||||
immutableCreate: true
|
||||
makeLatest: false
|
||||
name: ArchiSteamFarm V${{ github.ref_name }}
|
||||
prerelease: true
|
||||
|
||||
8
.github/workflows/translations.yml
vendored
8
.github/workflows/translations.yml
vendored
@@ -15,7 +15,7 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v4.2.2
|
||||
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
|
||||
with:
|
||||
show-progress: false
|
||||
submodules: recursive
|
||||
@@ -27,11 +27,11 @@ jobs:
|
||||
run: |
|
||||
set -eu
|
||||
|
||||
git fetch --depth=1 origin master
|
||||
git fetch origin master
|
||||
git reset --hard origin/master
|
||||
|
||||
- name: Download latest translations from Crowdin
|
||||
uses: crowdin/github-action@v2.5.2
|
||||
uses: crowdin/github-action@60debf382ee245b21794321190ad0501db89d8c1 # v2.13.0
|
||||
with:
|
||||
upload_sources: false
|
||||
download_translations: true
|
||||
@@ -43,7 +43,7 @@ jobs:
|
||||
token: ${{ secrets.ASF_CROWDIN_API_TOKEN }}
|
||||
|
||||
- name: Import GPG key for signing
|
||||
uses: crazy-max/ghaction-import-gpg@v6.2.0
|
||||
uses: crazy-max/ghaction-import-gpg@e89d40939c28e39f97cf32126055eeae86ba74ec # v6.3.0
|
||||
with:
|
||||
gpg_private_key: ${{ secrets.ARCHIBOT_GPG_PRIVATE_KEY }}
|
||||
git_config_global: true
|
||||
|
||||
2
ASF-ui
2
ASF-ui
Submodule ASF-ui updated: fbdc518fa1...c703223091
@@ -4,7 +4,8 @@
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="JetBrains.Annotations" PrivateAssets="all" />
|
||||
<PackageReference Include="JetBrains.Annotations.Sources" PrivateAssets="all" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.OpenApi" IncludeAssets="compile" />
|
||||
<PackageReference Include="SteamKit2" IncludeAssets="compile" />
|
||||
<PackageReference Include="System.Composition.AttributedModel" IncludeAssets="compile" />
|
||||
</ItemGroup>
|
||||
|
||||
@@ -4,7 +4,8 @@
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="JetBrains.Annotations" PrivateAssets="all" />
|
||||
<PackageReference Include="JetBrains.Annotations.Sources" PrivateAssets="all" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.OpenApi" IncludeAssets="compile" />
|
||||
<PackageReference Include="System.Composition.AttributedModel" IncludeAssets="compile" />
|
||||
</ItemGroup>
|
||||
|
||||
|
||||
@@ -4,8 +4,9 @@
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="AngleSharp.XPath" IncludeAssets="compile" />
|
||||
<PackageReference Include="JetBrains.Annotations" PrivateAssets="all" />
|
||||
<PackageReference Include="AngleSharp" IncludeAssets="compile" />
|
||||
<PackageReference Include="JetBrains.Annotations.Sources" PrivateAssets="all" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.OpenApi" IncludeAssets="compile" />
|
||||
<PackageReference Include="System.Composition.AttributedModel" IncludeAssets="compile" />
|
||||
</ItemGroup>
|
||||
|
||||
|
||||
@@ -66,7 +66,7 @@ public sealed class SignInWithSteamController : ArchiController {
|
||||
return StatusCode((int) HttpStatusCode.ServiceUnavailable, new GenericResponse(false, Strings.FormatErrorRequestFailedTooManyTimes(WebBrowser.MaxTries)));
|
||||
}
|
||||
|
||||
IAttr? paramsNode = challengeResponse.Content.SelectSingleNode<IAttr>("//input[@name='openidparams']/@value");
|
||||
IElement? paramsNode = challengeResponse.Content.QuerySelector("input[name='openidparams'][value]");
|
||||
|
||||
if (paramsNode == null) {
|
||||
ASF.ArchiLogger.LogNullError(paramsNode);
|
||||
@@ -74,7 +74,7 @@ public sealed class SignInWithSteamController : ArchiController {
|
||||
return StatusCode((int) HttpStatusCode.InternalServerError, new GenericResponse(false, Strings.FormatErrorObjectIsNull(nameof(paramsNode))));
|
||||
}
|
||||
|
||||
string paramsValue = paramsNode.Value;
|
||||
string? paramsValue = paramsNode.GetAttribute("value");
|
||||
|
||||
if (string.IsNullOrEmpty(paramsValue)) {
|
||||
ASF.ArchiLogger.LogNullError(paramsValue);
|
||||
@@ -82,7 +82,7 @@ public sealed class SignInWithSteamController : ArchiController {
|
||||
return StatusCode((int) HttpStatusCode.InternalServerError, new GenericResponse(false, Strings.FormatErrorObjectIsNull(nameof(paramsValue))));
|
||||
}
|
||||
|
||||
IAttr? nonceNode = challengeResponse.Content.SelectSingleNode<IAttr>("//input[@name='nonce']/@value");
|
||||
IElement? nonceNode = challengeResponse.Content.QuerySelector("input[name='nonce'][value]");
|
||||
|
||||
if (nonceNode == null) {
|
||||
ASF.ArchiLogger.LogNullError(nonceNode);
|
||||
@@ -90,7 +90,7 @@ public sealed class SignInWithSteamController : ArchiController {
|
||||
return StatusCode((int) HttpStatusCode.InternalServerError, new GenericResponse(false, Strings.FormatErrorObjectIsNull(nameof(nonceNode))));
|
||||
}
|
||||
|
||||
string nonceValue = nonceNode.Value;
|
||||
string? nonceValue = nonceNode.GetAttribute("value");
|
||||
|
||||
if (string.IsNullOrEmpty(nonceValue)) {
|
||||
ASF.ArchiLogger.LogNullError(nonceValue);
|
||||
|
||||
@@ -4,11 +4,11 @@
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="JetBrains.Annotations" PrivateAssets="all" />
|
||||
<PackageReference Include="JetBrains.Annotations.Sources" PrivateAssets="all" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.OpenApi" IncludeAssets="compile" />
|
||||
<PackageReference Include="Microsoft.CodeAnalysis.ResxSourceGenerator" PrivateAssets="all" />
|
||||
<PackageReference Include="SteamKit2" IncludeAssets="compile" />
|
||||
<PackageReference Include="System.Composition.AttributedModel" IncludeAssets="compile" />
|
||||
<PackageReference Include="System.Linq.Async" IncludeAssets="compile" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
|
||||
@@ -38,56 +38,54 @@ namespace ArchiSteamFarm.OfficialPlugins.ItemsMatcher;
|
||||
internal sealed class BotCache : SerializableFile {
|
||||
[JsonDisallowNull]
|
||||
[JsonInclude]
|
||||
[JsonObjectCreationHandling(JsonObjectCreationHandling.Populate)]
|
||||
internal ConcurrentList<AssetForListing> LastAnnouncedAssetsForListing { get; private init; } = [];
|
||||
|
||||
[JsonInclude]
|
||||
[JsonPropertyName("BackingLastAnnouncedTradeToken")]
|
||||
internal string? LastAnnouncedTradeToken {
|
||||
get => BackingLastAnnouncedTradeToken;
|
||||
get;
|
||||
|
||||
set {
|
||||
if (BackingLastAnnouncedTradeToken == value) {
|
||||
if (field == value) {
|
||||
return;
|
||||
}
|
||||
|
||||
BackingLastAnnouncedTradeToken = value;
|
||||
field = value;
|
||||
Utilities.InBackground(Save);
|
||||
}
|
||||
}
|
||||
|
||||
[JsonInclude]
|
||||
[JsonPropertyName("BackingLastInventoryChecksumBeforeDeduplication")]
|
||||
internal string? LastInventoryChecksumBeforeDeduplication {
|
||||
get => BackingLastInventoryChecksumBeforeDeduplication;
|
||||
get;
|
||||
|
||||
set {
|
||||
if (BackingLastInventoryChecksumBeforeDeduplication == value) {
|
||||
if (field == value) {
|
||||
return;
|
||||
}
|
||||
|
||||
BackingLastInventoryChecksumBeforeDeduplication = value;
|
||||
field = value;
|
||||
Utilities.InBackground(Save);
|
||||
}
|
||||
}
|
||||
|
||||
[JsonInclude]
|
||||
[JsonPropertyName("BackingLastRequestAt")]
|
||||
internal DateTime? LastRequestAt {
|
||||
get => BackingLastRequestAt;
|
||||
get;
|
||||
|
||||
set {
|
||||
if (BackingLastRequestAt == value) {
|
||||
if (field == value) {
|
||||
return;
|
||||
}
|
||||
|
||||
BackingLastRequestAt = value;
|
||||
field = 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);
|
||||
|
||||
@@ -97,18 +95,18 @@ internal sealed class BotCache : SerializableFile {
|
||||
[JsonConstructor]
|
||||
private BotCache() => LastAnnouncedAssetsForListing.OnModified += OnObjectModified;
|
||||
|
||||
[UsedImplicitly]
|
||||
public bool ShouldSerializeBackingLastAnnouncedTradeToken() => !string.IsNullOrEmpty(BackingLastAnnouncedTradeToken);
|
||||
|
||||
[UsedImplicitly]
|
||||
public bool ShouldSerializeBackingLastInventoryChecksumBeforeDeduplication() => !string.IsNullOrEmpty(BackingLastInventoryChecksumBeforeDeduplication);
|
||||
|
||||
[UsedImplicitly]
|
||||
public bool ShouldSerializeBackingLastRequestAt() => BackingLastRequestAt.HasValue;
|
||||
|
||||
[UsedImplicitly]
|
||||
public bool ShouldSerializeLastAnnouncedAssetsForListing() => LastAnnouncedAssetsForListing.Count > 0;
|
||||
|
||||
[UsedImplicitly]
|
||||
public bool ShouldSerializeLastAnnouncedTradeToken() => !string.IsNullOrEmpty(LastAnnouncedTradeToken);
|
||||
|
||||
[UsedImplicitly]
|
||||
public bool ShouldSerializeLastInventoryChecksumBeforeDeduplication() => !string.IsNullOrEmpty(LastInventoryChecksumBeforeDeduplication);
|
||||
|
||||
[UsedImplicitly]
|
||||
public bool ShouldSerializeLastRequestAt() => LastRequestAt.HasValue;
|
||||
|
||||
protected override void Dispose(bool disposing) {
|
||||
if (disposing) {
|
||||
// Events we registered
|
||||
|
||||
@@ -121,7 +121,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 = [..results.Where(static result => !string.IsNullOrEmpty(result))!];
|
||||
List<string> responses = [..results.Where(static result => !string.IsNullOrEmpty(result)).Select(static result => result!)];
|
||||
|
||||
return responses.Count > 0 ? string.Join(Environment.NewLine, responses) : null;
|
||||
}
|
||||
|
||||
@@ -64,6 +64,10 @@
|
||||
<value>Matched totalt {0} sæt denne runde.</value>
|
||||
<comment>{0} will be replaced by number of sets traded</comment>
|
||||
</data>
|
||||
<data name="ListingAnnouncing" xml:space="preserve">
|
||||
<value>Annoncerer {0} ({1}) med inventar lavet af {2} varer i alt på listen...</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>Matchede i alt {0} varer med bot {1} ({2}), sender handelstilbud...</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>
|
||||
|
||||
@@ -64,4 +64,16 @@
|
||||
<value>Ο τελικός αριθμός συνόλων που έχουν ταιριάξει είναι {0}, αυτόν τον γύρο.</value>
|
||||
<comment>{0} will be replaced by number of sets traded</comment>
|
||||
</data>
|
||||
<data name="ListingAnnouncing" xml:space="preserve">
|
||||
<value>Ανακοίνωση {0} ({1}) με απογραφή από στοιχεία {2} συνολικά στην καταχώρηση...</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>Ταίριαξε ένα σύνολο αντικειμένων {0} με bot {1} ({2}), στέλνοντας προσφορά συναλλαγής...</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>Αποτυχία αποστολής μιας προσφοράς συναλλαγής στο bot {0} ({1}), με κίνηση...</value>
|
||||
<comment>{0} will be replaced by steam ID (number), {1} will be replaced by user's nickname'</comment>
|
||||
</data>
|
||||
</root>
|
||||
|
||||
@@ -64,6 +64,10 @@
|
||||
<value>Abbinati un totale di {0} set questo round.</value>
|
||||
<comment>{0} will be replaced by number of sets traded</comment>
|
||||
</data>
|
||||
<data name="ListingAnnouncing" xml:space="preserve">
|
||||
<value>Annuncio {0} ({1}) con inventario fatto di elementi {2} in totale sulla lista...</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>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>
|
||||
|
||||
@@ -64,4 +64,20 @@
|
||||
<value>すべてのうち、{0} セットにマッチしたためこのラウンドを終了します。</value>
|
||||
<comment>{0} will be replaced by number of sets traded</comment>
|
||||
</data>
|
||||
<data name="ListingAnnouncing" xml:space="preserve">
|
||||
<value>{0}({1})をリスト上でアナウンス中です。インベントリには合計 {2} 個のアイテムが含まれています…</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>ボット {1}({2})と合計 {0} 個のアイテムが一致しました。トレードオファーを送信しています…</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>ボット {0} ({1})にトレードオファーを送信できませんでした。次に進みます…</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>いくつかの確認に失敗しました。 {0} の取引から約 {1} が正常に送信されました。</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>
|
||||
|
||||
@@ -60,4 +60,24 @@
|
||||
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089
|
||||
</value>
|
||||
</resheader>
|
||||
<data name="ActivelyMatchingItemsRound" xml:space="preserve">
|
||||
<value>Matchade totalt {0} sets denna runda.</value>
|
||||
<comment>{0} will be replaced by number of sets traded</comment>
|
||||
</data>
|
||||
<data name="ListingAnnouncing" xml:space="preserve">
|
||||
<value>Meddela {0} ({1}) med lager gjorda av {2} objekt totalt på listan...</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>Matchade totalt {0} objekt med bot {1} ({2}), skickar handels-erbjudande...</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>Det gick inte att skicka ett byteserbjudande till bot {0} ({1}), går vidare...</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>Vissa bekräftelser har misslyckats, cirka {0} av {1} skickades framgångsrikt.</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>
|
||||
|
||||
@@ -60,14 +60,14 @@ internal sealed class RemoteCommunication : IAsyncDisposable, IDisposable {
|
||||
private const byte MinHeartBeatTTL = 10; // Minimum amount of minutes we must wait before sending next HeartBeat
|
||||
private const byte MinimumPasswordResetCooldownDays = 5; // As imposed by Steam limits
|
||||
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 const byte MinPersonaStateTTL = MinAnnouncementTTL; // Minimum amount of minutes we must wait before requesting persona state update
|
||||
|
||||
private static readonly FrozenSet<EAssetType> AcceptedMatchableTypes = new HashSet<EAssetType>(4) {
|
||||
private static readonly FrozenSet<EAssetType> AcceptedMatchableTypes = [
|
||||
EAssetType.Emoticon,
|
||||
EAssetType.FoilTradingCard,
|
||||
EAssetType.ProfileBackground,
|
||||
EAssetType.TradingCard
|
||||
}.ToFrozenSet();
|
||||
];
|
||||
|
||||
private readonly Bot Bot;
|
||||
private readonly Timer? HeartBeatTimer;
|
||||
@@ -187,7 +187,7 @@ internal sealed class RemoteCommunication : IAsyncDisposable, IDisposable {
|
||||
return;
|
||||
}
|
||||
|
||||
if ((DateTime.UtcNow < LastAnnouncement.AddMinutes(ShouldSendAnnouncementEarlier ? MinAnnouncementTTL : MaxAnnouncementTTL)) && ShouldSendHeartBeats) {
|
||||
if (DateTime.UtcNow < LastAnnouncement.AddMinutes(ShouldSendAnnouncementEarlier ? MinAnnouncementTTL : MaxAnnouncementTTL)) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -199,7 +199,7 @@ internal sealed class RemoteCommunication : IAsyncDisposable, IDisposable {
|
||||
await RequestsSemaphore.WaitAsync().ConfigureAwait(false);
|
||||
|
||||
try {
|
||||
if ((DateTime.UtcNow < LastAnnouncement.AddMinutes(ShouldSendAnnouncementEarlier ? MinAnnouncementTTL : MaxAnnouncementTTL)) && ShouldSendHeartBeats) {
|
||||
if (DateTime.UtcNow < LastAnnouncement.AddMinutes(ShouldSendAnnouncementEarlier ? MinAnnouncementTTL : MaxAnnouncementTTL)) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -233,6 +233,7 @@ internal sealed class RemoteCommunication : IAsyncDisposable, IDisposable {
|
||||
HashSet<EAssetType> acceptedMatchableTypes = Bot.BotConfig.MatchableTypes.Where(AcceptedMatchableTypes.Contains).ToHashSet();
|
||||
|
||||
if (acceptedMatchableTypes.Count == 0) {
|
||||
// Should never happen, since IsEligibleForListing() check above ensured we have at least one matchable type
|
||||
throw new InvalidOperationException(nameof(acceptedMatchableTypes));
|
||||
}
|
||||
|
||||
@@ -905,9 +906,8 @@ internal sealed class RemoteCommunication : IAsyncDisposable, IDisposable {
|
||||
HashSet<EAssetType> acceptedMatchableTypes = Bot.BotConfig.MatchableTypes.Where(AcceptedMatchableTypes.Contains).ToHashSet();
|
||||
|
||||
if (acceptedMatchableTypes.Count == 0) {
|
||||
Bot.ArchiLogger.LogNullError(acceptedMatchableTypes);
|
||||
|
||||
return;
|
||||
// Should never happen, since IsEligibleForMatching() check above ensured we have at least one matchable type
|
||||
throw new InvalidOperationException(nameof(acceptedMatchableTypes));
|
||||
}
|
||||
|
||||
if (!await MatchActivelySemaphore.WaitAsync(0).ConfigureAwait(false)) {
|
||||
@@ -1276,20 +1276,7 @@ internal sealed class RemoteCommunication : IAsyncDisposable, IDisposable {
|
||||
|
||||
Bot.ArchiLogger.LogGenericTrace($"{listedUser.SteamID}...");
|
||||
|
||||
byte? tradeHoldDuration = await Bot.ArchiWebHandler.GetCombinedTradeHoldDurationAgainstUser(listedUser.SteamID, listedUser.TradeToken).ConfigureAwait(false);
|
||||
|
||||
switch (tradeHoldDuration) {
|
||||
case null:
|
||||
Bot.ArchiLogger.LogGenericTrace(Strings.FormatErrorIsEmpty(nameof(tradeHoldDuration)));
|
||||
|
||||
continue;
|
||||
case > 0 when (tradeHoldDuration.Value > maxTradeHoldDuration) || (tradeHoldDuration.Value > listedUser.MaxTradeHoldDuration):
|
||||
Bot.ArchiLogger.LogGenericTrace($"{tradeHoldDuration.Value} > {maxTradeHoldDuration} || {listedUser.MaxTradeHoldDuration}");
|
||||
|
||||
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 EAssetType.FoilTradingCard or EAssetType.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))).Select(static asset => asset.ToAsset()).ToHashSet();
|
||||
|
||||
if (theirInventory.Count == 0) {
|
||||
continue;
|
||||
@@ -1297,6 +1284,8 @@ internal sealed class RemoteCommunication : IAsyncDisposable, IDisposable {
|
||||
|
||||
skippedSetsThisUser.Clear();
|
||||
|
||||
byte? tradeHoldDuration = null;
|
||||
|
||||
Dictionary<(uint RealAppID, EAssetType Type, EAssetRarity Rarity), Dictionary<ulong, uint>> theirTradableState = MatchingUtilities.GetTradableInventoryState(theirInventory);
|
||||
|
||||
for (byte i = 0; i < Trading.MaxTradesPerAccount; i++) {
|
||||
@@ -1335,6 +1324,10 @@ internal sealed class RemoteCommunication : IAsyncDisposable, IDisposable {
|
||||
match = false;
|
||||
|
||||
foreach ((ulong ourItem, uint ourFullAmount) in ourFullSet.Where(static item => item.Value > 1).OrderByDescending(static item => item.Value)) {
|
||||
if ((tradeHoldDuration > maxTradeHoldDuration) || (tradeHoldDuration > listedUser.MaxTradeHoldDuration)) {
|
||||
break;
|
||||
}
|
||||
|
||||
if (!ourTradableSet.TryGetValue(ourItem, out uint ourTradableAmount) || (ourTradableAmount == 0)) {
|
||||
continue;
|
||||
}
|
||||
@@ -1378,6 +1371,29 @@ internal sealed class RemoteCommunication : IAsyncDisposable, IDisposable {
|
||||
}
|
||||
}
|
||||
|
||||
// Now that we have a match, check trade hold to ensure user is valid to match against
|
||||
// Since it involves remote call, we didn't do it previously, to skip checking against users with no matches for us
|
||||
if (tradeHoldDuration == null) {
|
||||
tradeHoldDuration = await Bot.ArchiWebHandler.GetCombinedTradeHoldDurationAgainstUser(listedUser.SteamID, listedUser.TradeToken).ConfigureAwait(false);
|
||||
|
||||
if (tradeHoldDuration == null) {
|
||||
Bot.ArchiLogger.LogGenericTrace(Strings.FormatErrorIsEmpty(nameof(tradeHoldDuration)));
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
if ((tradeHoldDuration.Value > maxTradeHoldDuration) || (tradeHoldDuration.Value > listedUser.MaxTradeHoldDuration)) {
|
||||
Bot.ArchiLogger.LogGenericTrace($"{tradeHoldDuration.Value} > {maxTradeHoldDuration} || {listedUser.MaxTradeHoldDuration}");
|
||||
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if ((tradeHoldDuration.Value > 0) && set.Type is EAssetType.FoilTradingCard or EAssetType.TradingCard && CardsFarmer.SalesBlacklist.Contains(set.RealAppID)) {
|
||||
// We're not considering this set for matching due to trade hold
|
||||
continue;
|
||||
}
|
||||
|
||||
// Skip this set from the remaining of this round
|
||||
skippedSetsThisTrade.Add(set);
|
||||
|
||||
|
||||
@@ -4,7 +4,8 @@
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="JetBrains.Annotations" PrivateAssets="all" />
|
||||
<PackageReference Include="JetBrains.Annotations.Sources" PrivateAssets="all" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.OpenApi" IncludeAssets="compile" />
|
||||
<PackageReference Include="Microsoft.CodeAnalysis.ResxSourceGenerator" PrivateAssets="all" />
|
||||
<PackageReference Include="SteamKit2" IncludeAssets="compile" />
|
||||
<PackageReference Include="System.Composition.AttributedModel" IncludeAssets="compile" />
|
||||
|
||||
@@ -135,12 +135,6 @@ internal static class Commands {
|
||||
|
||||
mobileAuthenticator.Init(bot);
|
||||
|
||||
MobileAuthenticatorHandler? mobileAuthenticatorHandler = bot.GetHandler<MobileAuthenticatorHandler>();
|
||||
|
||||
if (mobileAuthenticatorHandler == null) {
|
||||
throw new InvalidOperationException(nameof(mobileAuthenticatorHandler));
|
||||
}
|
||||
|
||||
ulong steamTime = await mobileAuthenticator.GetSteamTime().ConfigureAwait(false);
|
||||
|
||||
string? code = mobileAuthenticator.GenerateTokenForTime(steamTime);
|
||||
@@ -149,10 +143,10 @@ internal static class Commands {
|
||||
return bot.Commands.FormatBotResponse(Strings.FormatWarningFailedWithError(nameof(mobileAuthenticator.GenerateTokenForTime)));
|
||||
}
|
||||
|
||||
CTwoFactor_FinalizeAddAuthenticator_Response? response = await mobileAuthenticatorHandler.FinalizeAuthenticator(bot.SteamID, activationCode, code, steamTime).ConfigureAwait(false);
|
||||
CTwoFactor_FinalizeAddAuthenticator_Response? response = await MobileAuthenticatorWebHandler.FinalizeAuthenticator(bot, activationCode, code, steamTime).ConfigureAwait(false);
|
||||
|
||||
if (response == null) {
|
||||
return bot.Commands.FormatBotResponse(Strings.FormatWarningFailedWithError(nameof(mobileAuthenticatorHandler.FinalizeAuthenticator)));
|
||||
return bot.Commands.FormatBotResponse(Strings.FormatWarningFailedWithError(nameof(MobileAuthenticatorWebHandler.FinalizeAuthenticator)));
|
||||
}
|
||||
|
||||
if (!response.success) {
|
||||
@@ -198,7 +192,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 = [..results.Where(static result => !string.IsNullOrEmpty(result))!];
|
||||
List<string> responses = [..results.Where(static result => !string.IsNullOrEmpty(result)).Select(static result => result!)];
|
||||
|
||||
return responses.Count > 0 ? string.Join(Environment.NewLine, responses) : null;
|
||||
}
|
||||
@@ -295,7 +289,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 = [..results.Where(static result => !string.IsNullOrEmpty(result))!];
|
||||
List<string> responses = [..results.Where(static result => !string.IsNullOrEmpty(result)).Select(static result => result!)];
|
||||
|
||||
return responses.Count > 0 ? string.Join(Environment.NewLine, responses) : null;
|
||||
}
|
||||
@@ -319,18 +313,12 @@ internal static class Commands {
|
||||
return bot.Commands.FormatBotResponse(Strings.BotNotConnected);
|
||||
}
|
||||
|
||||
MobileAuthenticatorHandler? mobileAuthenticatorHandler = bot.GetHandler<MobileAuthenticatorHandler>();
|
||||
|
||||
if (mobileAuthenticatorHandler == null) {
|
||||
throw new InvalidOperationException(nameof(mobileAuthenticatorHandler));
|
||||
}
|
||||
|
||||
string deviceID = $"android:{Guid.NewGuid()}";
|
||||
|
||||
CTwoFactor_AddAuthenticator_Response? response = await mobileAuthenticatorHandler.AddAuthenticator(bot.SteamID, deviceID).ConfigureAwait(false);
|
||||
CTwoFactor_AddAuthenticator_Response? response = await MobileAuthenticatorWebHandler.AddAuthenticator(bot, deviceID).ConfigureAwait(false);
|
||||
|
||||
if (response == null) {
|
||||
return bot.Commands.FormatBotResponse(Strings.WarningFailed);
|
||||
return bot.Commands.FormatBotResponse(Strings.FormatWarningFailedWithError(nameof(MobileAuthenticatorWebHandler.AddAuthenticator)));
|
||||
}
|
||||
|
||||
EResult result = (EResult) response.status;
|
||||
@@ -374,7 +362,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 = [..results.Where(static result => !string.IsNullOrEmpty(result))!];
|
||||
List<string> responses = [..results.Where(static result => !string.IsNullOrEmpty(result)).Select(static result => result!)];
|
||||
|
||||
return responses.Count > 0 ? string.Join(Environment.NewLine, responses) : null;
|
||||
}
|
||||
|
||||
@@ -1,118 +0,0 @@
|
||||
// ----------------------------------------------------------------------------------------------
|
||||
// _ _ _ ____ _ _____
|
||||
// / \ _ __ ___ | |__ (_)/ ___| | |_ ___ __ _ _ __ ___ | ___|__ _ _ __ _ __ ___
|
||||
// / _ \ | '__|/ __|| '_ \ | |\___ \ | __|/ _ \ / _` || '_ ` _ \ | |_ / _` || '__|| '_ ` _ \
|
||||
// / ___ \ | | | (__ | | | || | ___) || |_| __/| (_| || | | | | || _|| (_| || | | | | | | |
|
||||
// /_/ \_\|_| \___||_| |_||_||____/ \__|\___| \__,_||_| |_| |_||_| \__,_||_| |_| |_| |_|
|
||||
// ----------------------------------------------------------------------------------------------
|
||||
// |
|
||||
// Copyright 2015-2025 Ł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.Threading.Tasks;
|
||||
using ArchiSteamFarm.Core;
|
||||
using ArchiSteamFarm.NLog;
|
||||
using SteamKit2;
|
||||
using SteamKit2.Internal;
|
||||
|
||||
namespace ArchiSteamFarm.OfficialPlugins.MobileAuthenticator;
|
||||
|
||||
internal sealed class MobileAuthenticatorHandler : ClientMsgHandler {
|
||||
private readonly ArchiLogger ArchiLogger;
|
||||
private readonly TwoFactor UnifiedTwoFactorService;
|
||||
|
||||
internal MobileAuthenticatorHandler(ArchiLogger archiLogger, SteamUnifiedMessages steamUnifiedMessages) {
|
||||
ArgumentNullException.ThrowIfNull(archiLogger);
|
||||
ArgumentNullException.ThrowIfNull(steamUnifiedMessages);
|
||||
|
||||
ArchiLogger = archiLogger;
|
||||
UnifiedTwoFactorService = steamUnifiedMessages.CreateService<TwoFactor>();
|
||||
}
|
||||
|
||||
public override void HandleMsg(IPacketMsg packetMsg) => ArgumentNullException.ThrowIfNull(packetMsg);
|
||||
|
||||
internal async Task<CTwoFactor_AddAuthenticator_Response?> AddAuthenticator(ulong steamID, string deviceID) {
|
||||
if ((steamID == 0) || !new SteamID(steamID).IsIndividualAccount) {
|
||||
throw new ArgumentOutOfRangeException(nameof(steamID));
|
||||
}
|
||||
|
||||
ArgumentException.ThrowIfNullOrEmpty(deviceID);
|
||||
|
||||
if (Client == null) {
|
||||
throw new InvalidOperationException(nameof(Client));
|
||||
}
|
||||
|
||||
if (!Client.IsConnected) {
|
||||
return null;
|
||||
}
|
||||
|
||||
CTwoFactor_AddAuthenticator_Request request = new() {
|
||||
authenticator_type = 1,
|
||||
authenticator_time = Utilities.GetUnixTime(),
|
||||
device_identifier = deviceID,
|
||||
steamid = steamID
|
||||
};
|
||||
|
||||
SteamUnifiedMessages.ServiceMethodResponse<CTwoFactor_AddAuthenticator_Response> response;
|
||||
|
||||
try {
|
||||
response = await UnifiedTwoFactorService.AddAuthenticator(request).ToLongRunningTask().ConfigureAwait(false);
|
||||
} catch (Exception e) {
|
||||
ArchiLogger.LogGenericWarningException(e);
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
return response.Result == EResult.OK ? response.Body : null;
|
||||
}
|
||||
|
||||
internal async Task<CTwoFactor_FinalizeAddAuthenticator_Response?> FinalizeAuthenticator(ulong steamID, string activationCode, string authenticatorCode, ulong authenticatorTime) {
|
||||
if ((steamID == 0) || !new SteamID(steamID).IsIndividualAccount) {
|
||||
throw new ArgumentOutOfRangeException(nameof(steamID));
|
||||
}
|
||||
|
||||
ArgumentException.ThrowIfNullOrEmpty(activationCode);
|
||||
ArgumentException.ThrowIfNullOrEmpty(authenticatorCode);
|
||||
ArgumentOutOfRangeException.ThrowIfNegativeOrZero(authenticatorTime);
|
||||
|
||||
if (Client == null) {
|
||||
throw new InvalidOperationException(nameof(Client));
|
||||
}
|
||||
|
||||
if (!Client.IsConnected) {
|
||||
return null;
|
||||
}
|
||||
|
||||
CTwoFactor_FinalizeAddAuthenticator_Request request = new() {
|
||||
activation_code = activationCode,
|
||||
authenticator_code = authenticatorCode,
|
||||
authenticator_time = authenticatorTime,
|
||||
steamid = steamID
|
||||
};
|
||||
|
||||
SteamUnifiedMessages.ServiceMethodResponse<CTwoFactor_FinalizeAddAuthenticator_Response> response;
|
||||
|
||||
try {
|
||||
response = await UnifiedTwoFactorService.FinalizeAddAuthenticator(request).ToLongRunningTask().ConfigureAwait(false);
|
||||
} catch (Exception e) {
|
||||
ArchiLogger.LogGenericWarningException(e);
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
return response.Result == EResult.OK ? response.Body : null;
|
||||
}
|
||||
}
|
||||
@@ -22,7 +22,6 @@
|
||||
// limitations under the License.
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.ComponentModel;
|
||||
using System.Composition;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
@@ -39,7 +38,7 @@ namespace ArchiSteamFarm.OfficialPlugins.MobileAuthenticator;
|
||||
|
||||
[Export(typeof(IPlugin))]
|
||||
[SuppressMessage("ReSharper", "MemberCanBeFileLocal")]
|
||||
internal sealed class MobileAuthenticatorPlugin : OfficialPlugin, IBotCommand2, IBotSteamClient {
|
||||
internal sealed class MobileAuthenticatorPlugin : OfficialPlugin, IBotCommand2 {
|
||||
[JsonInclude]
|
||||
public override string Name => nameof(MobileAuthenticatorPlugin);
|
||||
|
||||
@@ -66,25 +65,6 @@ internal sealed class MobileAuthenticatorPlugin : OfficialPlugin, IBotCommand2,
|
||||
return await Commands.OnBotCommand(bot, access, message, args, steamID).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
public Task OnBotSteamCallbacksInit(Bot bot, CallbackManager callbackManager) {
|
||||
ArgumentNullException.ThrowIfNull(bot);
|
||||
ArgumentNullException.ThrowIfNull(callbackManager);
|
||||
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
public Task<IReadOnlyCollection<ClientMsgHandler>?> OnBotSteamHandlersInit(Bot bot) {
|
||||
ArgumentNullException.ThrowIfNull(bot);
|
||||
|
||||
SteamUnifiedMessages? steamUnifiedMessages = bot.GetHandler<SteamUnifiedMessages>();
|
||||
|
||||
if (steamUnifiedMessages == null) {
|
||||
throw new InvalidOperationException(nameof(steamUnifiedMessages));
|
||||
}
|
||||
|
||||
return Task.FromResult<IReadOnlyCollection<ClientMsgHandler>?>(new HashSet<ClientMsgHandler>(1) { new MobileAuthenticatorHandler(bot.ArchiLogger, steamUnifiedMessages) });
|
||||
}
|
||||
|
||||
public override Task OnLoaded() {
|
||||
Utilities.WarnAboutIncompleteTranslation(Strings.ResourceManager);
|
||||
|
||||
|
||||
@@ -0,0 +1,174 @@
|
||||
// ----------------------------------------------------------------------------------------------
|
||||
// _ _ _ ____ _ _____
|
||||
// / \ _ __ ___ | |__ (_)/ ___| | |_ ___ __ _ _ __ ___ | ___|__ _ _ __ _ __ ___
|
||||
// / _ \ | '__|/ __|| '_ \ | |\___ \ | __|/ _ \ / _` || '_ ` _ \ | |_ / _` || '__|| '_ ` _ \
|
||||
// / ___ \ | | | (__ | | | || | ___) || |_| __/| (_| || | | | | || _|| (_| || | | | | | | |
|
||||
// /_/ \_\|_| \___||_| |_||_||____/ \__|\___| \__,_||_| |_| |_||_| \__,_||_| |_| |_| |_|
|
||||
// ----------------------------------------------------------------------------------------------
|
||||
// |
|
||||
// Copyright 2015-2025 Ł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.Net.Http;
|
||||
using System.Threading.Tasks;
|
||||
using ArchiSteamFarm.Core;
|
||||
using ArchiSteamFarm.Localization;
|
||||
using ArchiSteamFarm.Steam;
|
||||
using ArchiSteamFarm.Steam.Integration;
|
||||
using ArchiSteamFarm.Web;
|
||||
using SteamKit2;
|
||||
using SteamKit2.Internal;
|
||||
|
||||
namespace ArchiSteamFarm.OfficialPlugins.MobileAuthenticator;
|
||||
|
||||
internal static class MobileAuthenticatorWebHandler {
|
||||
private const string TwoFactorService = "ITwoFactorService";
|
||||
|
||||
internal static async Task<CTwoFactor_AddAuthenticator_Response?> AddAuthenticator(Bot bot, string deviceID) {
|
||||
ArgumentNullException.ThrowIfNull(bot);
|
||||
ArgumentException.ThrowIfNullOrEmpty(deviceID);
|
||||
|
||||
if (!bot.IsConnectedAndLoggedOn) {
|
||||
return null;
|
||||
}
|
||||
|
||||
string? accessToken = bot.AccessToken;
|
||||
|
||||
if (string.IsNullOrEmpty(accessToken)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const string endpoint = "AddAuthenticator";
|
||||
HttpMethod method = HttpMethod.Post;
|
||||
|
||||
CTwoFactor_AddAuthenticator_Request request = new() {
|
||||
authenticator_time = Utilities.GetUnixTime(),
|
||||
authenticator_type = 1,
|
||||
device_identifier = deviceID,
|
||||
steamid = bot.SteamID
|
||||
};
|
||||
|
||||
Dictionary<string, object?> arguments = new(1, StringComparer.Ordinal) {
|
||||
{ "access_token", accessToken }
|
||||
};
|
||||
|
||||
using WebAPI.AsyncInterface twoFactorService = bot.SteamConfiguration.GetAsyncWebAPIInterface(TwoFactorService);
|
||||
|
||||
twoFactorService.Timeout = bot.ArchiWebHandler.WebBrowser.Timeout;
|
||||
|
||||
WebAPI.WebAPIResponse<CTwoFactor_AddAuthenticator_Response>? response = null;
|
||||
|
||||
for (byte i = 0; (i < WebBrowser.MaxTries) && (response == null); i++) {
|
||||
if ((i > 0) && (ArchiWebHandler.WebLimiterDelay > 0)) {
|
||||
await Task.Delay(ArchiWebHandler.WebLimiterDelay).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
if (Debugging.IsUserDebugging) {
|
||||
bot.ArchiLogger.LogGenericDebug($"{method} {bot.SteamConfiguration.WebAPIBaseAddress}{TwoFactorService}/{endpoint}");
|
||||
}
|
||||
|
||||
try {
|
||||
response = await ArchiWebHandler.WebLimitRequest(
|
||||
bot.SteamConfiguration.WebAPIBaseAddress,
|
||||
|
||||
// ReSharper disable once AccessToDisposedClosure
|
||||
async () => await twoFactorService.CallProtobufAsync<CTwoFactor_AddAuthenticator_Response, CTwoFactor_AddAuthenticator_Request>(method, endpoint, request, extraArgs: arguments).ConfigureAwait(false)
|
||||
).ConfigureAwait(false);
|
||||
} catch (TaskCanceledException e) {
|
||||
bot.ArchiLogger.LogGenericDebuggingException(e);
|
||||
} catch (Exception e) {
|
||||
bot.ArchiLogger.LogGenericWarningException(e);
|
||||
}
|
||||
}
|
||||
|
||||
if (response == null) {
|
||||
bot.ArchiLogger.LogGenericWarning(Strings.FormatErrorRequestFailedTooManyTimes(WebBrowser.MaxTries));
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
return response.Body;
|
||||
}
|
||||
|
||||
internal static async Task<CTwoFactor_FinalizeAddAuthenticator_Response?> FinalizeAuthenticator(Bot bot, string activationCode, string authenticatorCode, ulong authenticatorTime) {
|
||||
ArgumentNullException.ThrowIfNull(bot);
|
||||
ArgumentException.ThrowIfNullOrEmpty(activationCode);
|
||||
ArgumentException.ThrowIfNullOrEmpty(authenticatorCode);
|
||||
ArgumentOutOfRangeException.ThrowIfZero(authenticatorTime);
|
||||
|
||||
if (!bot.IsConnectedAndLoggedOn) {
|
||||
return null;
|
||||
}
|
||||
|
||||
string? accessToken = bot.AccessToken;
|
||||
|
||||
if (string.IsNullOrEmpty(accessToken)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const string endpoint = "FinalizeAddAuthenticator";
|
||||
HttpMethod method = HttpMethod.Post;
|
||||
|
||||
CTwoFactor_FinalizeAddAuthenticator_Request request = new() {
|
||||
activation_code = activationCode,
|
||||
authenticator_code = authenticatorCode,
|
||||
authenticator_time = authenticatorTime,
|
||||
steamid = bot.SteamID
|
||||
};
|
||||
|
||||
Dictionary<string, object?> arguments = new(1, StringComparer.Ordinal) {
|
||||
{ "access_token", accessToken }
|
||||
};
|
||||
|
||||
using WebAPI.AsyncInterface twoFactorService = bot.SteamConfiguration.GetAsyncWebAPIInterface(TwoFactorService);
|
||||
|
||||
twoFactorService.Timeout = bot.ArchiWebHandler.WebBrowser.Timeout;
|
||||
|
||||
WebAPI.WebAPIResponse<CTwoFactor_FinalizeAddAuthenticator_Response>? response = null;
|
||||
|
||||
for (byte i = 0; (i < WebBrowser.MaxTries) && (response == null); i++) {
|
||||
if ((i > 0) && (ArchiWebHandler.WebLimiterDelay > 0)) {
|
||||
await Task.Delay(ArchiWebHandler.WebLimiterDelay).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
if (Debugging.IsUserDebugging) {
|
||||
bot.ArchiLogger.LogGenericDebug($"{method} {bot.SteamConfiguration.WebAPIBaseAddress}{TwoFactorService}/{endpoint}");
|
||||
}
|
||||
|
||||
try {
|
||||
response = await ArchiWebHandler.WebLimitRequest(
|
||||
bot.SteamConfiguration.WebAPIBaseAddress,
|
||||
|
||||
// ReSharper disable once AccessToDisposedClosure
|
||||
async () => await twoFactorService.CallProtobufAsync<CTwoFactor_FinalizeAddAuthenticator_Response, CTwoFactor_FinalizeAddAuthenticator_Request>(method, endpoint, request, extraArgs: arguments).ConfigureAwait(false)
|
||||
).ConfigureAwait(false);
|
||||
} catch (TaskCanceledException e) {
|
||||
bot.ArchiLogger.LogGenericDebuggingException(e);
|
||||
} catch (Exception e) {
|
||||
bot.ArchiLogger.LogGenericWarningException(e);
|
||||
}
|
||||
}
|
||||
|
||||
if (response == null) {
|
||||
bot.ArchiLogger.LogGenericWarning(Strings.FormatErrorRequestFailedTooManyTimes(WebBrowser.MaxTries));
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
return response.Body;
|
||||
}
|
||||
}
|
||||
@@ -5,7 +5,8 @@
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="JetBrains.Annotations" PrivateAssets="all" />
|
||||
<PackageReference Include="JetBrains.Annotations.Sources" PrivateAssets="all" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.OpenApi" IncludeAssets="compile" />
|
||||
<PackageReference Include="OpenTelemetry.Exporter.Prometheus.AspNetCore" />
|
||||
<PackageReference Include="OpenTelemetry.Extensions.Hosting" />
|
||||
<PackageReference Include="OpenTelemetry.Instrumentation.AspNetCore" />
|
||||
|
||||
@@ -133,8 +133,7 @@ internal sealed class MonitoringPlugin : OfficialPlugin, IBot, IBotTradeOfferRes
|
||||
throw new InvalidOperationException(nameof(Meter));
|
||||
}
|
||||
|
||||
services.AddOpenTelemetry().WithMetrics(
|
||||
builder => {
|
||||
services.AddOpenTelemetry().WithMetrics(builder => {
|
||||
builder.AddPrometheusExporter(static config => config.ScrapeEndpointPath = "/Api/metrics");
|
||||
builder.AddRuntimeInstrumentation();
|
||||
builder.AddAspNetCoreInstrumentation();
|
||||
@@ -153,11 +152,11 @@ internal sealed class MonitoringPlugin : OfficialPlugin, IBot, IBotTradeOfferRes
|
||||
|
||||
int officialPluginCount = PluginsCore.ActivePlugins.Count(static plugin => plugin is OfficialPlugin);
|
||||
|
||||
PluginMeasurements = new HashSet<Measurement<int>>(3) {
|
||||
new(PluginsCore.ActivePlugins.Count),
|
||||
new(officialPluginCount, new KeyValuePair<string, object?>(TagNames.PluginType, "official")),
|
||||
new(PluginsCore.ActivePlugins.Count - officialPluginCount, new KeyValuePair<string, object?>(TagNames.PluginType, "custom"))
|
||||
}.ToFrozenSet();
|
||||
PluginMeasurements = [
|
||||
new Measurement<int>(PluginsCore.ActivePlugins.Count),
|
||||
new Measurement<int>(officialPluginCount, new KeyValuePair<string, object?>(TagNames.PluginType, "official")),
|
||||
new Measurement<int>(PluginsCore.ActivePlugins.Count - officialPluginCount, new KeyValuePair<string, object?>(TagNames.PluginType, "custom"))
|
||||
];
|
||||
|
||||
Meter = new Meter(MeterName, Version.ToString());
|
||||
|
||||
@@ -272,8 +271,7 @@ internal sealed class MonitoringPlugin : OfficialPlugin, IBot, IBotTradeOfferRes
|
||||
);
|
||||
|
||||
Meter.CreateObservableCounter(
|
||||
$"{MetricNamePrefix}_bot_trades", () => TradeStatistics.SelectMany<KeyValuePair<Bot, TradeStatistics>, Measurement<int>>(
|
||||
static kv => [
|
||||
$"{MetricNamePrefix}_bot_trades", () => TradeStatistics.SelectMany<KeyValuePair<Bot, TradeStatistics>, Measurement<int>>(static kv => [
|
||||
new Measurement<int>(
|
||||
kv.Value.AcceptedOffers,
|
||||
new KeyValuePair<string, object?>(TagNames.BotName, kv.Key.BotName),
|
||||
|
||||
@@ -4,7 +4,8 @@
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="JetBrains.Annotations" PrivateAssets="all" />
|
||||
<PackageReference Include="JetBrains.Annotations.Sources" PrivateAssets="all" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.OpenApi" IncludeAssets="compile" />
|
||||
<PackageReference Include="Microsoft.CodeAnalysis.ResxSourceGenerator" PrivateAssets="all" />
|
||||
<PackageReference Include="SteamKit2" IncludeAssets="compile" />
|
||||
<PackageReference Include="System.Composition.AttributedModel" IncludeAssets="compile" />
|
||||
|
||||
@@ -107,7 +107,10 @@
|
||||
<value>Načítání informací o aplikaci {0} bylo dokončeno.</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>Dokončeno {0} z {1} požadavků na sklad</value>
|
||||
<comment>{0} will be replaced by the number (count this batch) of depot key requests that were successfully answered, {1} will be replaced by the number (count this batch) of depot key requests that were supposed to be sent</comment>
|
||||
</data>
|
||||
<data name="BotFinishedRetrievingTotalDepots" xml:space="preserve">
|
||||
<value>Načítání všech tokenbů úložišť, celkem z {0} aplikací bylo dokončeno.</value>
|
||||
<comment>{0} will be replaced by the number (total count) of apps retrieved</comment>
|
||||
|
||||
@@ -60,33 +60,115 @@
|
||||
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089
|
||||
</value>
|
||||
</resheader>
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
<data name="PluginDisabledMissingBuildToken" xml:space="preserve">
|
||||
<value>{0} er blevet deaktiveret på grund af en manglende build token</value>
|
||||
<comment>{0} will be replaced by the name of the plugin (e.g. "SteamTokenDumperPlugin")</comment>
|
||||
</data>
|
||||
<data name="PluginDisabledInConfig" xml:space="preserve">
|
||||
<value>{0} er i øjeblikket deaktiveret i henhold til din konfiguration. Hvis du vil hjælpe SteamDB i dataindsendelse, så tjek vores wiki.</value>
|
||||
<comment>{0} will be replaced by the name of the plugin (e.g. "SteamTokenDumperPlugin")</comment>
|
||||
</data>
|
||||
<data name="PluginInitializedAndEnabled" xml:space="preserve">
|
||||
<value>{0} er blevet initialiseret med succes, tak på forhånd for din hjælp. Den første indsendelse vil ske i cirka {1}.</value>
|
||||
<comment>{0} will be replaced by the name of the plugin (e.g. "SteamTokenDumperPlugin"), {1} will be replaced by translated TimeSpan string (such as "53 minutes")</comment>
|
||||
</data>
|
||||
<data name="FileCouldNotBeLoadedFreshInit" xml:space="preserve">
|
||||
<value>{0} kunne ikke indlæses, en ny instans vil blive initialiseret...</value>
|
||||
<comment>{0} will be replaced by the name of the file (e.g. "GlobalCache")</comment>
|
||||
</data>
|
||||
<data name="BotNoAppsToRefresh" xml:space="preserve">
|
||||
<value>Der er ingen apps, der kræver en opdatering i denne bot instans.</value>
|
||||
</data>
|
||||
<data name="BotRetrievingTotalAppAccessTokens" xml:space="preserve">
|
||||
<value>Henter i alt {0} app-adgangstokens...</value>
|
||||
<comment>{0} will be replaced by the number (total count) of app access tokens being retrieved</comment>
|
||||
</data>
|
||||
<data name="BotRetrievingAppAccessTokens" xml:space="preserve">
|
||||
<value>Henter {0} app-adgangstokens...</value>
|
||||
<comment>{0} will be replaced by the number (count this batch) of app access tokens being retrieved</comment>
|
||||
</data>
|
||||
<data name="BotFinishedRetrievingAppAccessTokens" xml:space="preserve">
|
||||
<value>Færdig med at hente {0} app-adgangstegn.</value>
|
||||
<comment>{0} will be replaced by the number (count this batch) of app access tokens retrieved</comment>
|
||||
</data>
|
||||
<data name="BotFinishedRetrievingTotalAppAccessTokens" xml:space="preserve">
|
||||
<value>Færdig med at hente i alt {0} app-adgangstokiner.</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>Henter alle depoter for i alt {0} apps...</value>
|
||||
<comment>{0} will be replaced by the number (total count) of apps being retrieved</comment>
|
||||
</data>
|
||||
<data name="BotRetrievingAppInfos" xml:space="preserve">
|
||||
<value>Retrieving {0} app infos...</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>Færdig med at hente {0} app-info.</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>Færdig {0} ud af {1} depot nøgleanmodninger.</value>
|
||||
<comment>{0} will be replaced by the number (count this batch) of depot key requests that were successfully answered, {1} will be replaced by the number (count this batch) of depot key requests that were supposed to be sent</comment>
|
||||
</data>
|
||||
<data name="BotFinishedRetrievingTotalDepots" xml:space="preserve">
|
||||
<value>Færdig med at hente alle depot nøgler for i alt {0} apps.</value>
|
||||
<comment>{0} will be replaced by the number (total count) of apps retrieved</comment>
|
||||
</data>
|
||||
<data name="SubmissionNoNewData" xml:space="preserve">
|
||||
<value>Der er ingen nye data at indsende, alt er opdateret.</value>
|
||||
</data>
|
||||
<data name="SubmissionNoContributorSet" xml:space="preserve">
|
||||
<value>Kunne ikke indsende data, da der ikke er noget gyldigt SteamID sæt, som vi kunne klassificere som bidragsyder. Overvej opsætning af {0} egenskab.</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>Indsender i alt registrerede apps/pakker/depots: {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>Indsendelsen er mislykkedes på grund af for mange anmodninger, vi vil prøve igen om cirka {0} fra nu.</value>
|
||||
<comment>{0} will be replaced by translated TimeSpan string (such as "53 minutes")</comment>
|
||||
</data>
|
||||
<data name="SubmissionSuccessful" xml:space="preserve">
|
||||
<value>Dataene er blevet indsendt. Serveren har registreret i alt nye apps/packages/depots: {0} ({1} verificeret)/{2} ({3} verificeret)/{4} ({5} verificeret).</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>Nye apps: {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>Verificerede apps: {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>Nye pakker: {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>Verificerede pakker: {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>Nye depoter: {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>Verificerede depot: {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} initialiseret, plugin vil ikke løse nogen af dem: {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>Indlæser STD global cache...</value>
|
||||
</data>
|
||||
<data name="ValidatingGlobalCacheIntegrity" xml:space="preserve">
|
||||
<value>Validerer STD global cache integritet...</value>
|
||||
</data>
|
||||
<data name="GlobalCacheIntegrityValidationFailed" xml:space="preserve">
|
||||
<value>Mislykkedes at verificere STD global cache integritet. Dette tyder på en potentiel fil / hukommelse korruption, en ny instans vil blive initialiseret i stedet.</value>
|
||||
</data>
|
||||
</root>
|
||||
|
||||
@@ -68,7 +68,10 @@
|
||||
<value>{0} είναι απενεργοποιημένο σύμφωνα με τις ρυθμίσεις σας. Αν θέλετε να βοηθήσετε το SteamDB στην υποβολή δεδομένων, παρακαλώ ελέγξτε το wiki μας.</value>
|
||||
<comment>{0} will be replaced by the name of the plugin (e.g. "SteamTokenDumperPlugin")</comment>
|
||||
</data>
|
||||
|
||||
<data name="PluginInitializedAndEnabled" xml:space="preserve">
|
||||
<value>Το {0} έχει αρχικοποιηθεί με επιτυχία, σας ευχαριστώ εκ των προτέρων για τη βοήθειά σας. Η πρώτη υποβολή θα γίνει σε περίπου {1}.</value>
|
||||
<comment>{0} will be replaced by the name of the plugin (e.g. "SteamTokenDumperPlugin"), {1} will be replaced by translated TimeSpan string (such as "53 minutes")</comment>
|
||||
</data>
|
||||
<data name="FileCouldNotBeLoadedFreshInit" xml:space="preserve">
|
||||
<value>{0} δεν μπόρεσε να φορτωθεί, μια νέα παρουσία θα αρχικοποιηθεί...</value>
|
||||
<comment>{0} will be replaced by the name of the file (e.g. "GlobalCache")</comment>
|
||||
@@ -104,7 +107,10 @@
|
||||
<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} από τα αιτήματα κλειδιού depot {1}.</value>
|
||||
<comment>{0} will be replaced by the number (count this batch) of depot key requests that were successfully answered, {1} will be replaced by the number (count this batch) of depot key requests that were supposed to be sent</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>
|
||||
|
||||
@@ -68,7 +68,10 @@
|
||||
<value>{0} on tällä hetkellä poistettu käytöstä asetuksistasi. Jos haluat auttaa SteamDB:tä tietojen lähettämisessä, ole hyvä ja tutustu wikimme.</value>
|
||||
<comment>{0} will be replaced by the name of the plugin (e.g. "SteamTokenDumperPlugin")</comment>
|
||||
</data>
|
||||
|
||||
<data name="PluginInitializedAndEnabled" xml:space="preserve">
|
||||
<value>{0} on alustettu onnistuneesti, kiitos etukäteen avustasi. Ensimmäiseen lähetykseen on noin {1}.</value>
|
||||
<comment>{0} will be replaced by the name of the plugin (e.g. "SteamTokenDumperPlugin"), {1} will be replaced by translated TimeSpan string (such as "53 minutes")</comment>
|
||||
</data>
|
||||
<data name="FileCouldNotBeLoadedFreshInit" xml:space="preserve">
|
||||
<value>{0} ei voitu ladata. Uusi instanssi alustetaan...</value>
|
||||
<comment>{0} will be replaced by the name of the file (e.g. "GlobalCache")</comment>
|
||||
@@ -104,7 +107,10 @@
|
||||
<value>Saatiin haettua {0} sovelluksen tiedot.</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>Suoritettiin {0}/{1} depot-avaimien pyynnöistä.</value>
|
||||
<comment>{0} will be replaced by the number (count this batch) of depot key requests that were successfully answered, {1} will be replaced by the number (count this batch) of depot key requests that were supposed to be sent</comment>
|
||||
</data>
|
||||
<data name="BotFinishedRetrievingTotalDepots" xml:space="preserve">
|
||||
<value>Saatiin haettua kaikki depot-avaimet yhteensä {0} sovellukselle.</value>
|
||||
<comment>{0} will be replaced by the number (total count) of apps retrieved</comment>
|
||||
|
||||
@@ -107,7 +107,10 @@
|
||||
<value>Récupération de {0} infos d'application terminée.</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>Récupération de {0} sur {1} demandes de clés de dépôt terminée.</value>
|
||||
<comment>{0} will be replaced by the number (count this batch) of depot key requests that were successfully answered, {1} will be replaced by the number (count this batch) of depot key requests that were supposed to be sent</comment>
|
||||
</data>
|
||||
<data name="BotFinishedRetrievingTotalDepots" xml:space="preserve">
|
||||
<value>Fin de la récupération de toutes les clés de dépôts pour un total de {0} applications.</value>
|
||||
<comment>{0} will be replaced by the number (total count) of apps retrieved</comment>
|
||||
|
||||
@@ -107,7 +107,10 @@
|
||||
<value>Selesai memuat {0} informasi aplikasi.</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>Selesai {0} dari {1} depot key yang di minta.</value>
|
||||
<comment>{0} will be replaced by the number (count this batch) of depot key requests that were successfully answered, {1} will be replaced by the number (count this batch) of depot key requests that were supposed to be sent</comment>
|
||||
</data>
|
||||
<data name="BotFinishedRetrievingTotalDepots" xml:space="preserve">
|
||||
<value>Selesai memuat semua kunci depot untuk total {0} aplikasi.</value>
|
||||
<comment>{0} will be replaced by the number (total count) of apps retrieved</comment>
|
||||
|
||||
@@ -107,7 +107,10 @@
|
||||
<value>Hai completato il recupero di {0} informazioni app.</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>Hai completato le richieste di chiave del deposito {0} su {1}.</value>
|
||||
<comment>{0} will be replaced by the number (count this batch) of depot key requests that were successfully answered, {1} will be replaced by the number (count this batch) of depot key requests that were supposed to be sent</comment>
|
||||
</data>
|
||||
<data name="BotFinishedRetrievingTotalDepots" xml:space="preserve">
|
||||
<value>Finito il recupero di tutte le chiavi del deposito per un totale di {0} applicazioni.</value>
|
||||
<comment>{0} will be replaced by the number (total count) of apps retrieved</comment>
|
||||
|
||||
@@ -60,25 +60,80 @@
|
||||
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089
|
||||
</value>
|
||||
</resheader>
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
<data name="PluginDisabledMissingBuildToken" xml:space="preserve">
|
||||
<value>ビルドトークンが見つからないため、{0} は無効化されました</value>
|
||||
<comment>{0} will be replaced by the name of the plugin (e.g. "SteamTokenDumperPlugin")</comment>
|
||||
</data>
|
||||
<data name="PluginDisabledInConfig" xml:space="preserve">
|
||||
<value>{0} は現在の設定により無効化されています。SteamDB へのデータ提供に協力いただける場合は、当プロジェクトの Wiki をご覧ください。</value>
|
||||
<comment>{0} will be replaced by the name of the plugin (e.g. "SteamTokenDumperPlugin")</comment>
|
||||
</data>
|
||||
<data name="PluginInitializedAndEnabled" xml:space="preserve">
|
||||
<value>{0} は正常に初期化されました。ご協力ありがとうございます。最初の送信はおおよそ {1} 後に行われます。</value>
|
||||
<comment>{0} will be replaced by the name of the plugin (e.g. "SteamTokenDumperPlugin"), {1} will be replaced by translated TimeSpan string (such as "53 minutes")</comment>
|
||||
</data>
|
||||
<data name="FileCouldNotBeLoadedFreshInit" xml:space="preserve">
|
||||
<value>{0} を読み込むことができませんでした。新しいインスタンスを初期化します…</value>
|
||||
<comment>{0} will be replaced by the name of the file (e.g. "GlobalCache")</comment>
|
||||
</data>
|
||||
<data name="BotNoAppsToRefresh" xml:space="preserve">
|
||||
<value>このボットでは更新が必要なアプリはありません。</value>
|
||||
</data>
|
||||
<data name="BotRetrievingTotalAppAccessTokens" xml:space="preserve">
|
||||
<value>合計 {0} 個のアプリアクセストークンを取得中…</value>
|
||||
<comment>{0} will be replaced by the number (total count) of app access tokens being retrieved</comment>
|
||||
</data>
|
||||
<data name="BotRetrievingAppAccessTokens" xml:space="preserve">
|
||||
<value>{0} 個のアプリアクセストークンを取得中…</value>
|
||||
<comment>{0} will be replaced by the number (count this batch) of app access tokens being retrieved</comment>
|
||||
</data>
|
||||
<data name="BotFinishedRetrievingAppAccessTokens" xml:space="preserve">
|
||||
<value>{0} 個のアプリアクセストークンの取得が完了しました。</value>
|
||||
<comment>{0} will be replaced by the number (count this batch) of app access tokens retrieved</comment>
|
||||
</data>
|
||||
<data name="BotFinishedRetrievingTotalAppAccessTokens" xml:space="preserve">
|
||||
<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>{1} 件中 {0} 件のデポキーリクエストが完了しました。</value>
|
||||
<comment>{0} will be replaced by the number (count this batch) of depot key requests that were successfully answered, {1} will be replaced by the number (count this batch) of depot key requests that were supposed to be sent</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>寄稿者として分類可能な有効な Steam Id が設定されていないため、データを送信できませんでした。{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>
|
||||
@@ -95,10 +150,25 @@
|
||||
<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>
|
||||
|
||||
@@ -121,7 +121,9 @@
|
||||
|
||||
|
||||
|
||||
|
||||
<data name="LoadingGlobalCache" xml:space="preserve">
|
||||
<value>STD 글로벌 캐시 로드 중...</value>
|
||||
</data>
|
||||
|
||||
|
||||
</root>
|
||||
|
||||
@@ -107,7 +107,10 @@
|
||||
<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>Pabeigts {0} no {1} noliktavas atslēgu pieprasījumiem.</value>
|
||||
<comment>{0} will be replaced by the number (count this batch) of depot key requests that were successfully answered, {1} will be replaced by the number (count this batch) of depot key requests that were supposed to be sent</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>
|
||||
|
||||
@@ -68,7 +68,10 @@
|
||||
<value>{0} is momenteel uitgeschakeld volgens uw configuratie. Als je SteamDB wilt helpen bij het indienen van gegevens, bekijk dan onze wiki.</value>
|
||||
<comment>{0} will be replaced by the name of the plugin (e.g. "SteamTokenDumperPlugin")</comment>
|
||||
</data>
|
||||
|
||||
<data name="PluginInitializedAndEnabled" xml:space="preserve">
|
||||
<value>{0} is met succes geïnitialiseerd. Alvast bedankt voor uw hulp. De eerste uitwerking zal gebeuren in ongeveer {1}.</value>
|
||||
<comment>{0} will be replaced by the name of the plugin (e.g. "SteamTokenDumperPlugin"), {1} will be replaced by translated TimeSpan string (such as "53 minutes")</comment>
|
||||
</data>
|
||||
<data name="FileCouldNotBeLoadedFreshInit" xml:space="preserve">
|
||||
<value>{0} kon niet worden geladen, een nieuwe instantie zal worden geïnitialiseerd...</value>
|
||||
<comment>{0} will be replaced by the name of the file (e.g. "GlobalCache")</comment>
|
||||
@@ -104,13 +107,33 @@
|
||||
<value>Ophalen van {0} app-infos voltooid.</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>Klaar {0} van {1} depot sleutelverzoeken.</value>
|
||||
<comment>{0} will be replaced by the number (count this batch) of depot key requests that were successfully answered, {1} will be replaced by the number (count this batch) of depot key requests that were supposed to be sent</comment>
|
||||
</data>
|
||||
<data name="BotFinishedRetrievingTotalDepots" xml:space="preserve">
|
||||
<value>Ophalen van alle depot keys voltooid voor een totaal van {0} apps.</value>
|
||||
<comment>{0} will be replaced by the number (total count) of apps retrieved</comment>
|
||||
</data>
|
||||
<data name="SubmissionNoNewData" xml:space="preserve">
|
||||
<value>Er zijn geen nieuwe gegevens om in te dienen, alles is up-to-date.</value>
|
||||
</data>
|
||||
<data name="SubmissionNoContributorSet" xml:space="preserve">
|
||||
<value>Kon de gegevens niet indienen omdat er geen geldige SteamID is die we als bijdrager kunnen classificeren. Overweeg het instellen van {0} property.</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>Verzenden van een totaal van geregistreerde apps/packages/depos: {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>De indiening is mislukt door te veel verzonden, we zullen het vanaf nu opnieuw proberen in ongeveer {0}.</value>
|
||||
<comment>{0} will be replaced by translated TimeSpan string (such as "53 minutes")</comment>
|
||||
</data>
|
||||
<data name="SubmissionSuccessful" xml:space="preserve">
|
||||
<value>De gegevens zijn succesvol ingediend. De server heeft een totaal van nieuwe apps/pakketten/depots: {0} ({1} geverifieerd)/{2} ({3} geverifieerd)/{4} ({5} geverifieerd).</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>Nieuwe apps: {0}</value>
|
||||
<comment>{0} will be replaced by list of the apps (IDs, numbers), separated by a comma</comment>
|
||||
@@ -127,10 +150,25 @@
|
||||
<value>Geverifieerde pakketten: {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>Nieuwe depoten: {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>Geverifieerde depot: {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} geïnitialiseerd, de plugin zal geen van deze oplossen: {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 global cache laden...</value>
|
||||
</data>
|
||||
<data name="ValidatingGlobalCacheIntegrity" xml:space="preserve">
|
||||
<value>Valideren van STD globale cache integriteit...</value>
|
||||
</data>
|
||||
<data name="GlobalCacheIntegrityValidationFailed" xml:space="preserve">
|
||||
<value>Kon de wereldwijde STD cache integriteit niet verifiëren. Dit suggereert een potentiële bestands/geheugencorruptie, een nieuw exemplaar zal in plaats daarvan worden geïnitialiseerd.</value>
|
||||
</data>
|
||||
</root>
|
||||
|
||||
@@ -60,33 +60,111 @@
|
||||
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089
|
||||
</value>
|
||||
</resheader>
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
<data name="PluginDisabledMissingBuildToken" xml:space="preserve">
|
||||
<value>{0} har blitt deaktivert på grunn av et manglende byggetoken</value>
|
||||
<comment>{0} will be replaced by the name of the plugin (e.g. "SteamTokenDumperPlugin")</comment>
|
||||
</data>
|
||||
<data name="PluginDisabledInConfig" xml:space="preserve">
|
||||
<value>{0} er for øyeblikket deaktivert i henhold til din konfigurasjon. Hvis du vil hjelpe SteamDB med datainnsending, vennligst sjekk ut vår wiki.</value>
|
||||
<comment>{0} will be replaced by the name of the plugin (e.g. "SteamTokenDumperPlugin")</comment>
|
||||
</data>
|
||||
<data name="PluginInitializedAndEnabled" xml:space="preserve">
|
||||
<value>{0} har blitt igangsatt vellykket, takk for din hjelp. Den første innsending vil skje i omtrent {1}.</value>
|
||||
<comment>{0} will be replaced by the name of the plugin (e.g. "SteamTokenDumperPlugin"), {1} will be replaced by translated TimeSpan string (such as "53 minutes")</comment>
|
||||
</data>
|
||||
<data name="FileCouldNotBeLoadedFreshInit" xml:space="preserve">
|
||||
<value>{0} kunne ikke lastes inn, en frisk forekomst vil bli initialisert...</value>
|
||||
<comment>{0} will be replaced by the name of the file (e.g. "GlobalCache")</comment>
|
||||
</data>
|
||||
<data name="BotNoAppsToRefresh" xml:space="preserve">
|
||||
<value>Det er ingen programmer som krever en oppdatering på denne botthendelsen.</value>
|
||||
</data>
|
||||
<data name="BotRetrievingTotalAppAccessTokens" xml:space="preserve">
|
||||
<value>Henter de totale {0} app-tilgangstegener...</value>
|
||||
<comment>{0} will be replaced by the number (total count) of app access tokens being retrieved</comment>
|
||||
</data>
|
||||
<data name="BotRetrievingAppAccessTokens" xml:space="preserve">
|
||||
<value>Henter {0} app-tilgangstokene...</value>
|
||||
<comment>{0} will be replaced by the number (count this batch) of app access tokens being retrieved</comment>
|
||||
</data>
|
||||
<data name="BotFinishedRetrievingAppAccessTokens" xml:space="preserve">
|
||||
<value>Ferdig med å hente {0} app-tilgangsnøkkler.</value>
|
||||
<comment>{0} will be replaced by the number (count this batch) of app access tokens retrieved</comment>
|
||||
</data>
|
||||
<data name="BotFinishedRetrievingTotalAppAccessTokens" xml:space="preserve">
|
||||
<value>Ferdig med å hente totalt {0} app-tilgangsnøkkler.</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>Henter alle depoter for en total {0} apper...</value>
|
||||
<comment>{0} will be replaced by the number (total count) of apps being retrieved</comment>
|
||||
</data>
|
||||
<data name="BotRetrievingAppInfos" xml:space="preserve">
|
||||
<value>Retrieving {0} app infos...</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>Ferdig med å hente {0} app infos.</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>Ferdig med {0} av {1} lagerets viktigste forespørsler.</value>
|
||||
<comment>{0} will be replaced by the number (count this batch) of depot key requests that were successfully answered, {1} will be replaced by the number (count this batch) of depot key requests that were supposed to be sent</comment>
|
||||
</data>
|
||||
<data name="BotFinishedRetrievingTotalDepots" xml:space="preserve">
|
||||
<value>Fullførte henting av alle depot-nøkler for totalt {0} apper.</value>
|
||||
<comment>{0} will be replaced by the number (total count) of apps retrieved</comment>
|
||||
</data>
|
||||
<data name="SubmissionNoNewData" xml:space="preserve">
|
||||
<value>Det finnes ingen nye data, alt er oppdatert.</value>
|
||||
</data>
|
||||
<data name="SubmissionNoContributorSet" xml:space="preserve">
|
||||
<value>Kunne ikke sende inn data fordi det ikke er gyldig SteamID satt som vi kan klassifisere som bidragsyter. Vurder å sette opp {0} egenskapen.</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="SubmissionFailedTooManyRequests" xml:space="preserve">
|
||||
<value>Innsendingen mislyktes på grunn av for mange forespørsler sendt, vi prøver igjen om lag {0} nå.</value>
|
||||
<comment>{0} will be replaced by translated TimeSpan string (such as "53 minutes")</comment>
|
||||
</data>
|
||||
<data name="SubmissionSuccessful" xml:space="preserve">
|
||||
<value>Dataene har blitt sendt inn. Serveren har registrert totalt nye applikasjoner/pakker/depoter: {0} ({1} verifisert)/{2} ({3} bekreftet)/{4} ({5} verifisert).</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>Nye apper: {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>Verifiserte apper: {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>Nye pakker: {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>Verifiserte pakker: {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>Nye depoter: {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>Verifiserte depoter: {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} initialisert, utvidelsen vil ikke løse noen av disse: {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>Laster STD global cache...</value>
|
||||
</data>
|
||||
<data name="ValidatingGlobalCacheIntegrity" xml:space="preserve">
|
||||
<value>Validerer STD global cache integritet...</value>
|
||||
</data>
|
||||
<data name="GlobalCacheIntegrityValidationFailed" xml:space="preserve">
|
||||
<value>Kan ikke verifisere STD globale cachens integritet. Dette foreslår en potensiell fil-/minnesorpsjon, et frisk eksempel vil bli initialisert i stedet.</value>
|
||||
</data>
|
||||
</root>
|
||||
|
||||
@@ -68,7 +68,10 @@
|
||||
<value>{0} está atualmente desativado de acordo com a sua configuração. Se quiser ajudar o SteamDB na submissão de dados, por favor confira o nosso wiki.</value>
|
||||
<comment>{0} will be replaced by the name of the plugin (e.g. "SteamTokenDumperPlugin")</comment>
|
||||
</data>
|
||||
|
||||
<data name="PluginInitializedAndEnabled" xml:space="preserve">
|
||||
<value>{0} foi inicializado com sucesso, obrigado antecipadamente por sua ajuda. O primeiro envio ocorrerá em aproximadamente {1}.</value>
|
||||
<comment>{0} will be replaced by the name of the plugin (e.g. "SteamTokenDumperPlugin"), {1} will be replaced by translated TimeSpan string (such as "53 minutes")</comment>
|
||||
</data>
|
||||
<data name="FileCouldNotBeLoadedFreshInit" xml:space="preserve">
|
||||
<value>{0} não pôde ser carregado, uma instância nova será inicializada...</value>
|
||||
<comment>{0} will be replaced by the name of the file (e.g. "GlobalCache")</comment>
|
||||
@@ -92,24 +95,80 @@
|
||||
<value>Terminado o recolhimento de um total de {0} tokens de acesso a aplicações.</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>Recuperando todos os depósitos para um total de apps {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>Retrieving {0} app infos...</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>Informações do aplicativo {0} finalizadas.</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} terminou de pedidos chave de depósito {1}.</value>
|
||||
<comment>{0} will be replaced by the number (count this batch) of depot key requests that were successfully answered, {1} will be replaced by the number (count this batch) of depot key requests that were supposed to be sent</comment>
|
||||
</data>
|
||||
<data name="BotFinishedRetrievingTotalDepots" xml:space="preserve">
|
||||
<value>Terminou de recuperar todas as chaves de depósito para um total de aplicativos {0}.</value>
|
||||
<comment>{0} will be replaced by the number (total count) of apps retrieved</comment>
|
||||
</data>
|
||||
<data name="SubmissionNoNewData" xml:space="preserve">
|
||||
<value>Não há novos dados para enviar, tudo está atualizado.</value>
|
||||
</data>
|
||||
<data name="SubmissionNoContributorSet" xml:space="preserve">
|
||||
<value>Não foi possível enviar os dados porque não há um conjunto SteamID válido que possamos classificar como colaborador. Considere configurar a propriedade {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>Enviar um total de apps/pacotes/pacotes registrados: {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>O envio falhou devido a muitas solicitações enviadas, tentaremos novamente em aproximadamente {0} a partir de agora.</value>
|
||||
<comment>{0} will be replaced by translated TimeSpan string (such as "53 minutes")</comment>
|
||||
</data>
|
||||
<data name="SubmissionSuccessful" xml:space="preserve">
|
||||
<value>Os dados foram enviados com sucesso. O servidor registrou um total de novos aplicativos/pacotes/depósitos: {0} ({1} verificado)/{2} ({3} verificado)/{4} ({5} verificado).</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>Novos aplicativos: {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>Apps verificados: {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>Novos pacotes: {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>Pacotes verificados: {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>Novos depósitos: {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>Depósitos verificados: {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} inicializado, o plugin não resolverá nenhum desses: {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>Carregando cache global STD...</value>
|
||||
</data>
|
||||
<data name="ValidatingGlobalCacheIntegrity" xml:space="preserve">
|
||||
<value>Validando integridade de cache global STD...</value>
|
||||
</data>
|
||||
<data name="GlobalCacheIntegrityValidationFailed" xml:space="preserve">
|
||||
<value>Falha ao verificar a integridade do cache global STD. Isso sugere uma potencial corrupção de arquivo/memória, uma instância nova será inicializada em vez disso.</value>
|
||||
</data>
|
||||
</root>
|
||||
|
||||
@@ -107,7 +107,10 @@
|
||||
<value>FINISHD RETRIEVIN {0} APP INFOS.</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>FINISHD {0} OUT OV {1} DEPOT KEY REQUESTS.</value>
|
||||
<comment>{0} will be replaced by the number (count this batch) of depot key requests that were successfully answered, {1} will be replaced by the number (count this batch) of depot key requests that were supposed to be sent</comment>
|
||||
</data>
|
||||
<data name="BotFinishedRetrievingTotalDepots" xml:space="preserve">
|
||||
<value>FINISHD RETRIEVIN ALL DEPOT KEYS 4 TOTAL OV {0} APPS.</value>
|
||||
<comment>{0} will be replaced by the number (total count) of apps retrieved</comment>
|
||||
|
||||
@@ -68,7 +68,10 @@
|
||||
<value>{0} este momentan dezactivat în funcție de configurația ta. Dacă doriți să ajutați SteamDB în transmiterea de date, vă rugăm să consultați wiki-ul.</value>
|
||||
<comment>{0} will be replaced by the name of the plugin (e.g. "SteamTokenDumperPlugin")</comment>
|
||||
</data>
|
||||
|
||||
<data name="PluginInitializedAndEnabled" xml:space="preserve">
|
||||
<value>{0} a fost inițializat cu succes, îți mulțumim anticipat pentru ajutorul tău. Prima trimitere va avea loc în aproximativ {1}.</value>
|
||||
<comment>{0} will be replaced by the name of the plugin (e.g. "SteamTokenDumperPlugin"), {1} will be replaced by translated TimeSpan string (such as "53 minutes")</comment>
|
||||
</data>
|
||||
<data name="FileCouldNotBeLoadedFreshInit" xml:space="preserve">
|
||||
<value>{0} nu a putut fi încărcat, o nouă instanță va fi inițializată...</value>
|
||||
<comment>{0} will be replaced by the name of the file (e.g. "GlobalCache")</comment>
|
||||
@@ -104,7 +107,10 @@
|
||||
<value>S-a terminat preluarea a {0} informații despre aplicații.</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>Am terminat {0} din {1} cereri de chei de depozit.</value>
|
||||
<comment>{0} will be replaced by the number (count this batch) of depot key requests that were successfully answered, {1} will be replaced by the number (count this batch) of depot key requests that were supposed to be sent</comment>
|
||||
</data>
|
||||
<data name="BotFinishedRetrievingTotalDepots" xml:space="preserve">
|
||||
<value>S-a terminat preluarea tuturor depot keys pentru un total de {0} aplicații.</value>
|
||||
<comment>{0} will be replaced by the number (total count) of apps retrieved</comment>
|
||||
|
||||
@@ -60,33 +60,115 @@
|
||||
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089
|
||||
</value>
|
||||
</resheader>
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
<data name="PluginDisabledMissingBuildToken" xml:space="preserve">
|
||||
<value>{0} har inaktiverats på grund av en saknad build-token</value>
|
||||
<comment>{0} will be replaced by the name of the plugin (e.g. "SteamTokenDumperPlugin")</comment>
|
||||
</data>
|
||||
<data name="PluginDisabledInConfig" xml:space="preserve">
|
||||
<value>{0} är för närvarande inaktiverat enligt din konfiguration. Om du vill hjälpa SteamDB i datainlämning, vänligen kolla in vår wiki.</value>
|
||||
<comment>{0} will be replaced by the name of the plugin (e.g. "SteamTokenDumperPlugin")</comment>
|
||||
</data>
|
||||
<data name="PluginInitializedAndEnabled" xml:space="preserve">
|
||||
<value>{0} har initierats framgångsrikt, tack i förväg för din hjälp. Den första inlämningen kommer att ske i ungefär {1}.</value>
|
||||
<comment>{0} will be replaced by the name of the plugin (e.g. "SteamTokenDumperPlugin"), {1} will be replaced by translated TimeSpan string (such as "53 minutes")</comment>
|
||||
</data>
|
||||
<data name="FileCouldNotBeLoadedFreshInit" xml:space="preserve">
|
||||
<value>{0} kunde inte laddas, en ny instans kommer att initieras...</value>
|
||||
<comment>{0} will be replaced by the name of the file (e.g. "GlobalCache")</comment>
|
||||
</data>
|
||||
<data name="BotNoAppsToRefresh" xml:space="preserve">
|
||||
<value>Det finns inga appar som kräver en uppdatering på denna bot instans.</value>
|
||||
</data>
|
||||
<data name="BotRetrievingTotalAppAccessTokens" xml:space="preserve">
|
||||
<value>Hämtar totalt {0} app access-tokens...</value>
|
||||
<comment>{0} will be replaced by the number (total count) of app access tokens being retrieved</comment>
|
||||
</data>
|
||||
<data name="BotRetrievingAppAccessTokens" xml:space="preserve">
|
||||
<value>Hämtar {0} app access-tokens...</value>
|
||||
<comment>{0} will be replaced by the number (count this batch) of app access tokens being retrieved</comment>
|
||||
</data>
|
||||
<data name="BotFinishedRetrievingAppAccessTokens" xml:space="preserve">
|
||||
<value>Slutförd hämtning av {0} app access-tokens.</value>
|
||||
<comment>{0} will be replaced by the number (count this batch) of app access tokens retrieved</comment>
|
||||
</data>
|
||||
<data name="BotFinishedRetrievingTotalAppAccessTokens" xml:space="preserve">
|
||||
<value>Slutförd hämtning av totalt {0} app access-tokens.</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>Hämtar alla depåer för totalt {0} appar...</value>
|
||||
<comment>{0} will be replaced by the number (total count) of apps being retrieved</comment>
|
||||
</data>
|
||||
<data name="BotRetrievingAppInfos" xml:space="preserve">
|
||||
<value>Hämtar {0} app-infon...</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>Slutade hämta {0} app-infon.</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>Avslutade {0} av {1} depå nyckel förfrågningar.</value>
|
||||
<comment>{0} will be replaced by the number (count this batch) of depot key requests that were successfully answered, {1} will be replaced by the number (count this batch) of depot key requests that were supposed to be sent</comment>
|
||||
</data>
|
||||
<data name="BotFinishedRetrievingTotalDepots" xml:space="preserve">
|
||||
<value>Slutförd hämtning av alla depå-nycklar för totalt {0} appar.</value>
|
||||
<comment>{0} will be replaced by the number (total count) of apps retrieved</comment>
|
||||
</data>
|
||||
<data name="SubmissionNoNewData" xml:space="preserve">
|
||||
<value>Det finns inget ny data att skicka in, allt är uppdaterat.</value>
|
||||
</data>
|
||||
<data name="SubmissionNoContributorSet" xml:space="preserve">
|
||||
<value>Kunde inte skicka in data eftersom det inte finns någon giltig SteamID som vi kunde klassificera som en bidragsgivare. Överväg att konfigurera {0} egenskap.</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>Skicka in totalt registrerade appar/paket/depåer: {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>Inlämningen misslyckades på grund av för många förfrågningar skickade, vi försöker igen om ungefär {0} från nu.</value>
|
||||
<comment>{0} will be replaced by translated TimeSpan string (such as "53 minutes")</comment>
|
||||
</data>
|
||||
<data name="SubmissionSuccessful" xml:space="preserve">
|
||||
<value>Data har lyckats överlämnas. Servern har registrerat totalt nya appar/packet/depåer: {0} ({1} verifierad)/{2} ({3} verifierad)/{4} ({5} verifierad).</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>Nya appar: {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>Verifierade appar: {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>Nya paket: {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>Verifierade paket: {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>Nya depåer: {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>Verifierade depåer: {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} initierad, plugin kommer inte att lösa någon av dessa: {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>Laddar STD global cache...</value>
|
||||
</data>
|
||||
<data name="ValidatingGlobalCacheIntegrity" xml:space="preserve">
|
||||
<value>Validerar STD global cache integritet...</value>
|
||||
</data>
|
||||
<data name="GlobalCacheIntegrityValidationFailed" xml:space="preserve">
|
||||
<value>Det gick inte att verifiera STD global cache integritet. Detta tyder på en potentiell fil/minnes korruption, en ny instans kommer att initieras istället.</value>
|
||||
</data>
|
||||
</root>
|
||||
|
||||
@@ -69,7 +69,7 @@
|
||||
<comment>{0} will be replaced by the name of the plugin (e.g. "SteamTokenDumperPlugin")</comment>
|
||||
</data>
|
||||
<data name="PluginInitializedAndEnabled" xml:space="preserve">
|
||||
<value>{0} başarıyla başlatıldı, yardımınız için şimdiden teşekkürler. İlk sunum yaklaşık olarak {1} içinde gerçekleşecektir.</value>
|
||||
<value>{0} başarıyla başlatıldı, yardımınız için şimdiden teşekkürler. İlk gönderim yaklaşık olarak {1} içinde gerçekleşecektir.</value>
|
||||
<comment>{0} will be replaced by the name of the plugin (e.g. "SteamTokenDumperPlugin"), {1} will be replaced by translated TimeSpan string (such as "53 minutes")</comment>
|
||||
</data>
|
||||
<data name="FileCouldNotBeLoadedFreshInit" xml:space="preserve">
|
||||
|
||||
@@ -68,7 +68,10 @@
|
||||
<value>{0} наразі вимкнено відповідно до вашої конфігурації. Якщо ви хочете допомогти SteamDB у надсиланні даних, перегляньте нашу вікі.</value>
|
||||
<comment>{0} will be replaced by the name of the plugin (e.g. "SteamTokenDumperPlugin")</comment>
|
||||
</data>
|
||||
|
||||
<data name="PluginInitializedAndEnabled" xml:space="preserve">
|
||||
<value>{0} було успішно ініціалізовано, заздалегідь дякуємо за вашу допомогу. Перше подання відбудеться приблизно через {1}.</value>
|
||||
<comment>{0} will be replaced by the name of the plugin (e.g. "SteamTokenDumperPlugin"), {1} will be replaced by translated TimeSpan string (such as "53 minutes")</comment>
|
||||
</data>
|
||||
<data name="FileCouldNotBeLoadedFreshInit" xml:space="preserve">
|
||||
<value>{0} не вдалося завантажити, буде ініціалізований новий екземпляр...</value>
|
||||
<comment>{0} will be replaced by the name of the file (e.g. "GlobalCache")</comment>
|
||||
@@ -104,7 +107,10 @@
|
||||
<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 key requests that were successfully answered, {1} will be replaced by the number (count this batch) of depot key requests that were supposed to be sent</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>
|
||||
|
||||
@@ -25,7 +25,6 @@ namespace ArchiSteamFarm.OfficialPlugins.SteamTokenDumper;
|
||||
|
||||
internal static class SharedInfo {
|
||||
internal const byte ApiVersion = 2;
|
||||
internal const byte AppInfosPerSingleRequest = byte.MaxValue;
|
||||
internal const byte HoursBetweenUploads = 24;
|
||||
internal const byte MaximumHoursBetweenRefresh = 8; // Per single bot account, makes sense to be 2 or 3 times less than MinimumHoursBetweenUploads
|
||||
internal const byte MaximumMinutesBeforeFirstUpload = 60; // Must be greater or equal to MinimumMinutesBeforeFirstUpload
|
||||
|
||||
@@ -360,42 +360,26 @@ internal sealed class SteamTokenDumperPlugin : OfficialPlugin, IASF, IBot, IBotC
|
||||
|
||||
bot.ArchiLogger.LogGenericInfo(Strings.FormatBotRetrievingTotalAppAccessTokens(appIDsToRefresh.Count));
|
||||
|
||||
HashSet<uint> appIDsThisRound = new(Math.Min(appIDsToRefresh.Count, SharedInfo.AppInfosPerSingleRequest));
|
||||
|
||||
using (HashSet<uint>.Enumerator enumerator = appIDsToRefresh.GetEnumerator()) {
|
||||
while (true) {
|
||||
if (!bot.IsConnectedAndLoggedOn) {
|
||||
return;
|
||||
}
|
||||
|
||||
while ((appIDsThisRound.Count < SharedInfo.AppInfosPerSingleRequest) && enumerator.MoveNext()) {
|
||||
appIDsThisRound.Add(enumerator.Current);
|
||||
}
|
||||
|
||||
if (appIDsThisRound.Count == 0) {
|
||||
break;
|
||||
}
|
||||
|
||||
bot.ArchiLogger.LogGenericInfo(Strings.FormatBotRetrievingAppAccessTokens(appIDsThisRound.Count));
|
||||
|
||||
SteamApps.PICSTokensCallback response;
|
||||
|
||||
try {
|
||||
response = await bot.SteamApps.PICSGetAccessTokens(appIDsThisRound, []).ToLongRunningTask().ConfigureAwait(false);
|
||||
} catch (Exception e) {
|
||||
bot.ArchiLogger.LogGenericWarningException(e);
|
||||
|
||||
appIDsThisRound.Clear();
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
bot.ArchiLogger.LogGenericInfo(Strings.FormatBotFinishedRetrievingAppAccessTokens(appIDsThisRound.Count));
|
||||
|
||||
appIDsThisRound.Clear();
|
||||
|
||||
GlobalCache.UpdateAppTokens(response.AppTokens, response.AppTokensDenied);
|
||||
foreach (uint[] appIDsThisRound in appIDsToRefresh.Chunk(Bot.EntriesPerSinglePICSRequest)) {
|
||||
if (!bot.IsConnectedAndLoggedOn) {
|
||||
return;
|
||||
}
|
||||
|
||||
bot.ArchiLogger.LogGenericInfo(Strings.FormatBotRetrievingAppAccessTokens(appIDsThisRound.Length));
|
||||
|
||||
SteamApps.PICSTokensCallback response;
|
||||
|
||||
try {
|
||||
response = await bot.SteamApps.PICSGetAccessTokens(appIDsThisRound, []).ToLongRunningTask().ConfigureAwait(false);
|
||||
} catch (Exception e) {
|
||||
bot.ArchiLogger.LogGenericWarningException(e);
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
bot.ArchiLogger.LogGenericInfo(Strings.FormatBotFinishedRetrievingAppAccessTokens(appIDsThisRound.Length));
|
||||
|
||||
GlobalCache.UpdateAppTokens(response.AppTokens, response.AppTokensDenied);
|
||||
}
|
||||
|
||||
bot.ArchiLogger.LogGenericInfo(Strings.FormatBotFinishedRetrievingTotalAppAccessTokens(appIDsToRefresh.Count));
|
||||
@@ -403,131 +387,113 @@ internal sealed class SteamTokenDumperPlugin : OfficialPlugin, IASF, IBot, IBotC
|
||||
|
||||
(_, FrozenSet<uint>? knownDepotIDs) = await GlobalCache.KnownDepotIDs.GetValue(ECacheFallback.SuccessPreviously).ConfigureAwait(false);
|
||||
|
||||
using (HashSet<uint>.Enumerator enumerator = appIDsToRefresh.GetEnumerator()) {
|
||||
while (true) {
|
||||
if (!bot.IsConnectedAndLoggedOn) {
|
||||
return;
|
||||
}
|
||||
foreach (uint[] appIDsThisRound in appIDsToRefresh.Chunk(Bot.EntriesPerSinglePICSRequest)) {
|
||||
if (!bot.IsConnectedAndLoggedOn) {
|
||||
return;
|
||||
}
|
||||
|
||||
while ((appIDsThisRound.Count < SharedInfo.AppInfosPerSingleRequest) && enumerator.MoveNext()) {
|
||||
appIDsThisRound.Add(enumerator.Current);
|
||||
}
|
||||
bot.ArchiLogger.LogGenericInfo(Strings.FormatBotRetrievingAppInfos(appIDsThisRound.Length));
|
||||
|
||||
if (appIDsThisRound.Count == 0) {
|
||||
break;
|
||||
}
|
||||
AsyncJobMultiple<SteamApps.PICSProductInfoCallback>.ResultSet response;
|
||||
|
||||
bot.ArchiLogger.LogGenericInfo(Strings.FormatBotRetrievingAppInfos(appIDsThisRound.Count));
|
||||
try {
|
||||
response = await bot.SteamApps.PICSGetProductInfo(appIDsThisRound.Select(static appID => new SteamApps.PICSRequest(appID, GlobalCache.GetAppToken(appID))), []).ToLongRunningTask().ConfigureAwait(false);
|
||||
} catch (Exception e) {
|
||||
bot.ArchiLogger.LogGenericWarningException(e);
|
||||
|
||||
AsyncJobMultiple<SteamApps.PICSProductInfoCallback>.ResultSet response;
|
||||
continue;
|
||||
}
|
||||
|
||||
try {
|
||||
response = await bot.SteamApps.PICSGetProductInfo(appIDsThisRound.Select(static appID => new SteamApps.PICSRequest(appID, GlobalCache.GetAppToken(appID))), []).ToLongRunningTask().ConfigureAwait(false);
|
||||
} catch (Exception e) {
|
||||
bot.ArchiLogger.LogGenericWarningException(e);
|
||||
if (response.Results == null) {
|
||||
bot.ArchiLogger.LogGenericWarning(ArchiSteamFarm.Localization.Strings.FormatWarningFailedWithError(nameof(response.Results)));
|
||||
|
||||
appIDsThisRound.Clear();
|
||||
continue;
|
||||
}
|
||||
|
||||
continue;
|
||||
}
|
||||
bot.ArchiLogger.LogGenericInfo(Strings.FormatBotFinishedRetrievingAppInfos(appIDsThisRound.Length));
|
||||
|
||||
if (response.Results == null) {
|
||||
bot.ArchiLogger.LogGenericWarning(ArchiSteamFarm.Localization.Strings.FormatWarningFailedWithError(nameof(response.Results)));
|
||||
Dictionary<uint, uint> appChangeNumbers = new();
|
||||
|
||||
appIDsThisRound.Clear();
|
||||
uint depotKeysSuccessful = 0;
|
||||
uint depotKeysTotal = 0;
|
||||
|
||||
continue;
|
||||
}
|
||||
foreach (SteamApps.PICSProductInfoCallback.PICSProductInfo app in response.Results.SelectMany(static result => result.Apps.Values)) {
|
||||
appChangeNumbers[app.ID] = app.ChangeNumber;
|
||||
|
||||
bot.ArchiLogger.LogGenericInfo(Strings.FormatBotFinishedRetrievingAppInfos(appIDsThisRound.Count));
|
||||
bool shouldFetchMainKey = false;
|
||||
|
||||
appIDsThisRound.Clear();
|
||||
foreach (KeyValue depot in app.KeyValues["depots"].Children) {
|
||||
if (!uint.TryParse(depot.Name, out uint depotID) || (knownDepotIDs?.Contains(depotID) == true) || (Config?.SecretDepotIDs.Contains(depotID) == true) || !GlobalCache.ShouldRefreshDepotKey(depotID)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
Dictionary<uint, uint> appChangeNumbers = new();
|
||||
depotKeysTotal++;
|
||||
|
||||
uint depotKeysSuccessful = 0;
|
||||
uint depotKeysTotal = 0;
|
||||
await depotsRateLimitingSemaphore.WaitAsync().ConfigureAwait(false);
|
||||
|
||||
foreach (SteamApps.PICSProductInfoCallback.PICSProductInfo app in response.Results.SelectMany(static result => result.Apps.Values)) {
|
||||
appChangeNumbers[app.ID] = app.ChangeNumber;
|
||||
try {
|
||||
SteamApps.DepotKeyCallback depotResponse = await bot.SteamApps.GetDepotDecryptionKey(depotID, app.ID).ToLongRunningTask().ConfigureAwait(false);
|
||||
|
||||
bool shouldFetchMainKey = false;
|
||||
depotKeysSuccessful++;
|
||||
|
||||
foreach (KeyValue depot in app.KeyValues["depots"].Children) {
|
||||
if (!uint.TryParse(depot.Name, out uint depotID) || (knownDepotIDs?.Contains(depotID) == true) || (Config?.SecretDepotIDs.Contains(depotID) == true) || !GlobalCache.ShouldRefreshDepotKey(depotID)) {
|
||||
if (depotResponse.Result != EResult.OK) {
|
||||
continue;
|
||||
}
|
||||
|
||||
depotKeysTotal++;
|
||||
shouldFetchMainKey = true;
|
||||
|
||||
await depotsRateLimitingSemaphore.WaitAsync().ConfigureAwait(false);
|
||||
GlobalCache.UpdateDepotKey(depotResponse);
|
||||
} catch (Exception e) {
|
||||
// We can still try other depots
|
||||
bot.ArchiLogger.LogGenericWarningException(e);
|
||||
} finally {
|
||||
Utilities.InBackground(async () => {
|
||||
await Task.Delay(DepotsRateLimitingDelay).ConfigureAwait(false);
|
||||
|
||||
try {
|
||||
SteamApps.DepotKeyCallback depotResponse = await bot.SteamApps.GetDepotDecryptionKey(depotID, app.ID).ToLongRunningTask().ConfigureAwait(false);
|
||||
|
||||
depotKeysSuccessful++;
|
||||
|
||||
if (depotResponse.Result != EResult.OK) {
|
||||
continue;
|
||||
// ReSharper disable once AccessToDisposedClosure - we're waiting for the semaphore to be free before disposing it
|
||||
depotsRateLimitingSemaphore.Release();
|
||||
}
|
||||
|
||||
shouldFetchMainKey = true;
|
||||
|
||||
GlobalCache.UpdateDepotKey(depotResponse);
|
||||
} catch (Exception e) {
|
||||
// We can still try other depots
|
||||
bot.ArchiLogger.LogGenericWarningException(e);
|
||||
} finally {
|
||||
Utilities.InBackground(
|
||||
async () => {
|
||||
await Task.Delay(DepotsRateLimitingDelay).ConfigureAwait(false);
|
||||
|
||||
// ReSharper disable once AccessToDisposedClosure - we're waiting for the semaphore to be free before disposing it
|
||||
depotsRateLimitingSemaphore.Release();
|
||||
}
|
||||
);
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Consider fetching main appID key only if we've actually considered some new depots for resolving
|
||||
if (shouldFetchMainKey && (knownDepotIDs?.Contains(app.ID) != true) && GlobalCache.ShouldRefreshDepotKey(app.ID)) {
|
||||
// Consider fetching main appID key only if we've actually considered some new depots for resolving
|
||||
if (shouldFetchMainKey && (knownDepotIDs?.Contains(app.ID) != true) && GlobalCache.ShouldRefreshDepotKey(app.ID)) {
|
||||
await depotsRateLimitingSemaphore.WaitAsync().ConfigureAwait(false);
|
||||
|
||||
try {
|
||||
SteamApps.DepotKeyCallback depotResponse = await bot.SteamApps.GetDepotDecryptionKey(app.ID, app.ID).ToLongRunningTask().ConfigureAwait(false);
|
||||
|
||||
// Increment total in combination with successful, we allow this one to fail on us
|
||||
depotKeysTotal++;
|
||||
depotKeysSuccessful++;
|
||||
|
||||
await depotsRateLimitingSemaphore.WaitAsync().ConfigureAwait(false);
|
||||
GlobalCache.UpdateDepotKey(depotResponse);
|
||||
} catch (Exception e) {
|
||||
// We can still try other depots
|
||||
bot.ArchiLogger.LogGenericWarningException(e);
|
||||
} finally {
|
||||
Utilities.InBackground(async () => {
|
||||
await Task.Delay(DepotsRateLimitingDelay).ConfigureAwait(false);
|
||||
|
||||
try {
|
||||
SteamApps.DepotKeyCallback depotResponse = await bot.SteamApps.GetDepotDecryptionKey(app.ID, app.ID).ToLongRunningTask().ConfigureAwait(false);
|
||||
|
||||
depotKeysSuccessful++;
|
||||
|
||||
GlobalCache.UpdateDepotKey(depotResponse);
|
||||
} catch (Exception e) {
|
||||
// We can still try other depots
|
||||
bot.ArchiLogger.LogGenericWarningException(e);
|
||||
} finally {
|
||||
Utilities.InBackground(
|
||||
async () => {
|
||||
await Task.Delay(DepotsRateLimitingDelay).ConfigureAwait(false);
|
||||
|
||||
// ReSharper disable once AccessToDisposedClosure - we're waiting for the semaphore to be free before disposing it
|
||||
depotsRateLimitingSemaphore.Release();
|
||||
}
|
||||
);
|
||||
}
|
||||
// ReSharper disable once AccessToDisposedClosure - we're waiting for the semaphore to be free before disposing it
|
||||
depotsRateLimitingSemaphore.Release();
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if (depotKeysTotal > 0) {
|
||||
bot.ArchiLogger.LogGenericInfo(Strings.FormatBotFinishedRetrievingDepotKeys(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
|
||||
continue;
|
||||
}
|
||||
|
||||
GlobalCache.UpdateAppChangeNumbers(appChangeNumbers);
|
||||
}
|
||||
|
||||
if (depotKeysTotal > 0) {
|
||||
bot.ArchiLogger.LogGenericInfo(Strings.FormatBotFinishedRetrievingDepotKeys(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
|
||||
continue;
|
||||
}
|
||||
|
||||
GlobalCache.UpdateAppChangeNumbers(appChangeNumbers);
|
||||
}
|
||||
|
||||
bot.ArchiLogger.LogGenericInfo(Strings.FormatBotFinishedRetrievingTotalDepots(appIDsToRefresh.Count));
|
||||
@@ -561,8 +527,7 @@ internal sealed class SteamTokenDumperPlugin : OfficialPlugin, IASF, IBot, IBotC
|
||||
return bot.Commands.FormatBotResponse(ArchiSteamFarm.Localization.Strings.FormatWarningFailedWithError(nameof(GlobalCache)));
|
||||
}
|
||||
|
||||
Utilities.InBackground(
|
||||
async () => {
|
||||
Utilities.InBackground(async () => {
|
||||
await Refresh(bot).ConfigureAwait(false);
|
||||
await SubmitData().ConfigureAwait(false);
|
||||
}
|
||||
@@ -598,8 +563,7 @@ internal sealed class SteamTokenDumperPlugin : OfficialPlugin, IASF, IBot, IBotC
|
||||
return Commands.FormatStaticResponse(ArchiSteamFarm.Localization.Strings.FormatWarningFailedWithError(nameof(GlobalCache)));
|
||||
}
|
||||
|
||||
Utilities.InBackground(
|
||||
async () => {
|
||||
Utilities.InBackground(async () => {
|
||||
await Utilities.InParallel(bots.Select(static bot => Refresh(bot))).ConfigureAwait(false);
|
||||
|
||||
await SubmitData().ConfigureAwait(false);
|
||||
|
||||
@@ -36,7 +36,7 @@ internal sealed class ArchiCryptoHelper {
|
||||
|
||||
[DataRow(ECryptoMethod.PlainText)]
|
||||
[DataRow(ECryptoMethod.AES)]
|
||||
[DataTestMethod]
|
||||
[TestMethod]
|
||||
internal async Task CanEncryptDecrypt(ECryptoMethod cryptoMethod) {
|
||||
if (!Enum.IsDefined(cryptoMethod)) {
|
||||
throw new InvalidEnumArgumentException(nameof(cryptoMethod), (int) cryptoMethod, typeof(ECryptoMethod));
|
||||
@@ -54,12 +54,28 @@ internal sealed class ArchiCryptoHelper {
|
||||
|
||||
[TestMethod]
|
||||
internal async Task CanEncryptDecryptProtectedDataForCurrentUser() {
|
||||
// Not supported on other platforms than Windows
|
||||
if (!OperatingSystem.IsWindows()) {
|
||||
// Not supported on other platforms than Windows
|
||||
return;
|
||||
Assert.Inconclusive($"!{nameof(OperatingSystem.IsWindows)}");
|
||||
}
|
||||
|
||||
await CanEncryptDecrypt(ECryptoMethod.ProtectedDataForCurrentUser).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
[DataRow(EHashingMethod.PlainText, TestPassword)]
|
||||
[DataRow(EHashingMethod.Pbkdf2, "WlS48GNrs1hAhcNHPfV09TPTLhf03gExb6zpaKiwX5A=")]
|
||||
[DataRow(EHashingMethod.SCrypt, "9LjhjyugakDQ7Haq/ufyTZDfIGeeWbLcE+/9IeKm8gc=")]
|
||||
[TestMethod]
|
||||
internal void CanHash(EHashingMethod hashingMethod, string expectedHash) {
|
||||
if (!Enum.IsDefined(hashingMethod)) {
|
||||
throw new InvalidEnumArgumentException(nameof(hashingMethod), (int) hashingMethod, typeof(EHashingMethod));
|
||||
}
|
||||
|
||||
ArgumentException.ThrowIfNullOrEmpty(expectedHash);
|
||||
|
||||
string hashed = Hash(hashingMethod, TestPassword);
|
||||
|
||||
Assert.AreEqual(expectedHash, hashed);
|
||||
}
|
||||
}
|
||||
#pragma warning restore CA1812 // False positive, the class is used during MSTest
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="JetBrains.Annotations" PrivateAssets="all" />
|
||||
<PackageReference Include="JetBrains.Annotations.Sources" PrivateAssets="all" />
|
||||
<PackageReference Include="MSTest" />
|
||||
</ItemGroup>
|
||||
|
||||
|
||||
@@ -26,7 +26,6 @@ using System.Collections.Generic;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.Linq;
|
||||
using System.Reflection;
|
||||
using System.Text.Json;
|
||||
using System.Text.Json.Nodes;
|
||||
using ArchiSteamFarm.Core;
|
||||
using ArchiSteamFarm.Helpers.Json;
|
||||
@@ -50,7 +49,7 @@ internal sealed class Bot {
|
||||
throw new InvalidOperationException(nameof(constructor));
|
||||
}
|
||||
|
||||
JsonElement emptyObject = new JsonObject().ToJsonElement();
|
||||
JsonObject emptyObject = new();
|
||||
|
||||
BotConfig? botConfig = emptyObject.ToJsonObject<BotConfig>();
|
||||
|
||||
@@ -106,7 +105,7 @@ internal sealed class Bot {
|
||||
CreateCard(2, realAppID: appID)
|
||||
];
|
||||
|
||||
Assert.ThrowsException<ArgumentOutOfRangeException>(() => GetItemsForFullBadge(items, 2, appID, MinCardsPerBadge - 1));
|
||||
Assert.ThrowsExactly<ArgumentOutOfRangeException>(() => GetItemsForFullBadge(items, 2, appID, MinCardsPerBadge - 1));
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
@@ -472,7 +471,7 @@ internal sealed class Bot {
|
||||
|
||||
HashSet<Asset> itemsToSend = GetItemsForFullBadge(items, 2, appID);
|
||||
|
||||
Assert.IsTrue(itemsToSend.Count <= Steam.Exchange.Trading.MaxItemsPerTrade);
|
||||
Assert.IsLessThanOrEqualTo(Steam.Exchange.Trading.MaxItemsPerTrade, itemsToSend.Count);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
@@ -496,7 +495,7 @@ internal sealed class Bot {
|
||||
|
||||
HashSet<Asset> itemsToSend = GetItemsForFullBadge(items, itemsPerSet);
|
||||
|
||||
Assert.IsTrue(itemsToSend.Count <= Steam.Exchange.Trading.MaxItemsPerTrade);
|
||||
Assert.IsLessThanOrEqualTo(Steam.Exchange.Trading.MaxItemsPerTrade, itemsToSend.Count);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
@@ -512,8 +511,7 @@ internal sealed class Bot {
|
||||
CreateCard(4, realAppID: appID0)
|
||||
];
|
||||
|
||||
Assert.ThrowsException<InvalidOperationException>(
|
||||
() => GetItemsForFullBadge(
|
||||
Assert.ThrowsExactly<InvalidOperationException>(() => GetItemsForFullBadge(
|
||||
items, new Dictionary<uint, byte> {
|
||||
{ appID0, 3 },
|
||||
{ appID1, 3 },
|
||||
@@ -528,7 +526,7 @@ internal sealed class Bot {
|
||||
ArgumentNullException.ThrowIfNull(itemsToSend);
|
||||
|
||||
Dictionary<(uint RealAppID, ulong ContextID, ulong ClassID), long> realResult = itemsToSend.GroupBy(static asset => (asset.RealAppID, asset.ContextID, asset.ClassID)).ToDictionary(static group => group.Key, static group => group.Sum(static asset => asset.Amount));
|
||||
Assert.AreEqual(expectedResult.Count, realResult.Count);
|
||||
Assert.HasCount(expectedResult.Count, realResult);
|
||||
Assert.IsTrue(expectedResult.All(expectation => realResult.TryGetValue(expectation.Key, out long reality) && (expectation.Value == reality)));
|
||||
}
|
||||
|
||||
|
||||
96
ArchiSteamFarm.Tests/IGitHubPluginUpdates.cs
Normal file
96
ArchiSteamFarm.Tests/IGitHubPluginUpdates.cs
Normal file
@@ -0,0 +1,96 @@
|
||||
// ----------------------------------------------------------------------------------------------
|
||||
// _ _ _ ____ _ _____
|
||||
// / \ _ __ ___ | |__ (_)/ ___| | |_ ___ __ _ _ __ ___ | ___|__ _ _ __ _ __ ___
|
||||
// / _ \ | '__|/ __|| '_ \ | |\___ \ | __|/ _ \ / _` || '_ ` _ \ | |_ / _` || '__|| '_ ` _ \
|
||||
// / ___ \ | | | (__ | | | || | ___) || |_| __/| (_| || | | | | || _|| (_| || | | | | | | |
|
||||
// /_/ \_\|_| \___||_| |_||_||____/ \__|\___| \__,_||_| |_| |_||_| \__,_||_| |_| |_| |_|
|
||||
// ----------------------------------------------------------------------------------------------
|
||||
// |
|
||||
// Copyright 2015-2025 Ł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.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using ArchiSteamFarm.Core;
|
||||
using ArchiSteamFarm.Localization;
|
||||
using ArchiSteamFarm.NLog;
|
||||
using ArchiSteamFarm.Storage;
|
||||
using ArchiSteamFarm.Web;
|
||||
using ArchiSteamFarm.Web.GitHub;
|
||||
using ArchiSteamFarm.Web.GitHub.Data;
|
||||
using JetBrains.Annotations;
|
||||
using Microsoft.VisualStudio.TestTools.UnitTesting;
|
||||
|
||||
namespace ArchiSteamFarm.Tests;
|
||||
|
||||
#pragma warning disable CA1812 // False positive, the class is used during MSTest
|
||||
[TestClass]
|
||||
internal sealed class IGitHubPluginUpdates {
|
||||
private const string PluginName = "ArchiSteamFarm.OfficialPlugins.Monitoring";
|
||||
private const string Repository = "JustArchiNET/ArchiSteamFarm";
|
||||
|
||||
private readonly TestContext TestContext;
|
||||
|
||||
private CancellationToken CancellationToken => TestContext.CancellationToken;
|
||||
|
||||
[UsedImplicitly]
|
||||
public IGitHubPluginUpdates(TestContext testContext) {
|
||||
ArgumentNullException.ThrowIfNull(testContext);
|
||||
|
||||
TestContext = testContext;
|
||||
}
|
||||
|
||||
[TestCategory("Manual")]
|
||||
[TestMethod]
|
||||
internal async Task DoesNotOfferPointlessUpdatesWhenMultipleAssetsAreFound() {
|
||||
using WebBrowser webBrowser = new(new ArchiLogger("Test"));
|
||||
|
||||
typeof(ASF).GetProperty(nameof(ASF.WebBrowser))?.SetValue(null, webBrowser);
|
||||
|
||||
ReleaseResponse? response = await GitHubService.GetLatestRelease(Repository, cancellationToken: CancellationToken).ConfigureAwait(false);
|
||||
|
||||
if (response == null) {
|
||||
Assert.Inconclusive(Strings.FormatWarningFailedWithError(nameof(response)));
|
||||
}
|
||||
|
||||
Version version = Version.Parse(response.Tag);
|
||||
|
||||
Plugins.Interfaces.IGitHubPluginUpdates plugin = new TestGitHubPluginUpdates(version);
|
||||
|
||||
Uri? releaseURL = await plugin.GetTargetReleaseURL(version, BuildInfo.Variant, true, GlobalConfig.EUpdateChannel.Stable, false).ConfigureAwait(false);
|
||||
|
||||
Assert.IsNull(releaseURL);
|
||||
|
||||
Uri? forcedReleaseURL = await plugin.GetTargetReleaseURL(version, BuildInfo.Variant, true, GlobalConfig.EUpdateChannel.Stable, true).ConfigureAwait(false);
|
||||
|
||||
Assert.IsNotNull(forcedReleaseURL);
|
||||
}
|
||||
|
||||
private sealed class TestGitHubPluginUpdates : Plugins.Interfaces.IGitHubPluginUpdates {
|
||||
public string Name => PluginName;
|
||||
public string RepositoryName => Repository;
|
||||
public Version Version { get; }
|
||||
|
||||
internal TestGitHubPluginUpdates(Version version) {
|
||||
ArgumentNullException.ThrowIfNull(version);
|
||||
|
||||
Version = version;
|
||||
}
|
||||
|
||||
public Task OnLoaded() => Task.CompletedTask;
|
||||
}
|
||||
}
|
||||
#pragma warning restore CA1812 // False positive, the class is used during MSTest
|
||||
@@ -36,7 +36,7 @@ internal sealed class MobileAuthenticator {
|
||||
[DataRow("qrg+wW8/u/TDt2i/+FQuPhuVrmY=", (ulong) 1337, "mYbCKs8ZvsVN2odCMxpvidrIu1c=", "conf")]
|
||||
[DataRow("qrg+wW8/u/TDt2i/+FQuPhuVrmY=", (ulong) 1723332288, "hiEx+JBqJqFJnSSL+dEthPHOmsc=")]
|
||||
[DataRow("qrg+wW8/u/TDt2i/+FQuPhuVrmY=", (ulong) 1723332288, "hpZUxyNgwBvtKPROvedjuvVPQiE=", "conf")]
|
||||
[DataTestMethod]
|
||||
[TestMethod]
|
||||
internal void GenerateConfirmationHash(string identitySecret, ulong time, string expectedCode, string? tag = null) {
|
||||
ArgumentException.ThrowIfNullOrEmpty(identitySecret);
|
||||
ArgumentOutOfRangeException.ThrowIfZero(time);
|
||||
@@ -58,7 +58,7 @@ internal sealed class MobileAuthenticator {
|
||||
|
||||
[DataRow("KDHC3rsY8+CmiswnXJcE5e5dRfd=", (ulong) 1337, "47J4D")]
|
||||
[DataRow("KDHC3rsY8+CmiswnXJcE5e5dRfd=", (ulong) 1723332288, "JQ3HQ")]
|
||||
[DataTestMethod]
|
||||
[TestMethod]
|
||||
internal void GenerateTokenForTime(string sharedSecret, ulong time, string expectedCode) {
|
||||
ArgumentException.ThrowIfNullOrEmpty(sharedSecret);
|
||||
ArgumentOutOfRangeException.ThrowIfZero(time);
|
||||
@@ -81,7 +81,7 @@ internal sealed class MobileAuthenticator {
|
||||
["shared_secret"] = sharedSecret
|
||||
};
|
||||
|
||||
Steam.Security.MobileAuthenticator? result = jsonObject.ToJsonElement().ToJsonObject<Steam.Security.MobileAuthenticator>();
|
||||
Steam.Security.MobileAuthenticator? result = jsonObject.ToJsonObject<Steam.Security.MobileAuthenticator>();
|
||||
|
||||
if (result == null) {
|
||||
throw new InvalidOperationException(nameof(result));
|
||||
|
||||
@@ -25,7 +25,9 @@ using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using JetBrains.Annotations;
|
||||
using Microsoft.VisualStudio.TestTools.UnitTesting;
|
||||
using static ArchiSteamFarm.Steam.Integration.SteamChatMessage;
|
||||
|
||||
@@ -34,6 +36,17 @@ namespace ArchiSteamFarm.Tests;
|
||||
#pragma warning disable CA1812 // False positive, the class is used during MSTest
|
||||
[TestClass]
|
||||
internal sealed class SteamChatMessage {
|
||||
private readonly TestContext TestContext;
|
||||
|
||||
private CancellationToken CancellationToken => TestContext.CancellationToken;
|
||||
|
||||
[UsedImplicitly]
|
||||
public SteamChatMessage(TestContext testContext) {
|
||||
ArgumentNullException.ThrowIfNull(testContext);
|
||||
|
||||
TestContext = testContext;
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
internal async Task CanSplitEvenWithStupidlyLongPrefix() {
|
||||
string prefix = new('x', MaxMessagePrefixBytes);
|
||||
@@ -41,9 +54,9 @@ internal sealed class SteamChatMessage {
|
||||
const string emoji = "😎";
|
||||
const string message = $"{emoji}{emoji}{emoji}{emoji}";
|
||||
|
||||
List<string> output = await GetMessageParts(message, prefix, true).ToListAsync().ConfigureAwait(false);
|
||||
List<string> output = await GetMessageParts(message, prefix, true).ToListAsync(CancellationToken).ConfigureAwait(false);
|
||||
|
||||
Assert.AreEqual(4, output.Count);
|
||||
Assert.HasCount(4, output);
|
||||
|
||||
Assert.AreEqual($"{prefix}{emoji}{ContinuationCharacter}", output[0]);
|
||||
Assert.AreEqual($"{prefix}{ContinuationCharacter}{emoji}{ContinuationCharacter}", output[1]);
|
||||
@@ -58,15 +71,15 @@ internal sealed class SteamChatMessage {
|
||||
internal async Task DoesntSkipEmptyNewlines() {
|
||||
string message = $"asdf{Environment.NewLine}{Environment.NewLine}asdf";
|
||||
|
||||
List<string> output = await GetMessageParts(message).ToListAsync().ConfigureAwait(false);
|
||||
List<string> output = await GetMessageParts(message).ToListAsync(CancellationToken).ConfigureAwait(false);
|
||||
|
||||
Assert.AreEqual(1, output.Count);
|
||||
Assert.HasCount(1, output);
|
||||
Assert.AreEqual(message, output.First());
|
||||
}
|
||||
|
||||
[DataRow(false)]
|
||||
[DataRow(true)]
|
||||
[DataTestMethod]
|
||||
[TestMethod]
|
||||
internal async Task DoesntSplitInTheMiddleOfMultiByteChar(bool isAccountLimited) {
|
||||
int maxMessageBytes = isAccountLimited ? MaxMessageBytesForLimitedAccounts : MaxMessageBytesForUnlimitedAccounts;
|
||||
int longLineLength = maxMessageBytes - ReservedContinuationMessageBytes;
|
||||
@@ -76,9 +89,9 @@ internal sealed class SteamChatMessage {
|
||||
string longSequence = new('a', longLineLength - 1);
|
||||
string message = $"{longSequence}{emoji}";
|
||||
|
||||
List<string> output = await GetMessageParts(message, isAccountLimited: isAccountLimited).ToListAsync().ConfigureAwait(false);
|
||||
List<string> output = await GetMessageParts(message, isAccountLimited: isAccountLimited).ToListAsync(CancellationToken).ConfigureAwait(false);
|
||||
|
||||
Assert.AreEqual(2, output.Count);
|
||||
Assert.HasCount(2, output);
|
||||
|
||||
Assert.AreEqual($"{longSequence}{ContinuationCharacter}", output[0]);
|
||||
Assert.AreEqual($"{ContinuationCharacter}{emoji}", output[1]);
|
||||
@@ -89,15 +102,15 @@ internal sealed class SteamChatMessage {
|
||||
const string message = "abcdef[";
|
||||
const string escapedMessage = @"abcdef\[";
|
||||
|
||||
List<string> output = await GetMessageParts(message).ToListAsync().ConfigureAwait(false);
|
||||
List<string> output = await GetMessageParts(message).ToListAsync(CancellationToken).ConfigureAwait(false);
|
||||
|
||||
Assert.AreEqual(1, output.Count);
|
||||
Assert.HasCount(1, output);
|
||||
Assert.AreEqual(escapedMessage, output.First());
|
||||
}
|
||||
|
||||
[DataRow(false)]
|
||||
[DataRow(true)]
|
||||
[DataTestMethod]
|
||||
[TestMethod]
|
||||
internal async Task DoesntSplitOnBackslashNotUsedForEscaping(bool isAccountLimited) {
|
||||
int maxMessageBytes = isAccountLimited ? MaxMessageBytesForLimitedAccounts : MaxMessageBytesForUnlimitedAccounts;
|
||||
int longLineLength = maxMessageBytes - ReservedContinuationMessageBytes;
|
||||
@@ -105,15 +118,15 @@ internal sealed class SteamChatMessage {
|
||||
string longLine = new('a', longLineLength - 2);
|
||||
string message = $@"{longLine}\";
|
||||
|
||||
List<string> output = await GetMessageParts(message, isAccountLimited: isAccountLimited).ToListAsync().ConfigureAwait(false);
|
||||
List<string> output = await GetMessageParts(message, isAccountLimited: isAccountLimited).ToListAsync(CancellationToken).ConfigureAwait(false);
|
||||
|
||||
Assert.AreEqual(1, output.Count);
|
||||
Assert.HasCount(1, output);
|
||||
Assert.AreEqual($@"{message}\", output.First());
|
||||
}
|
||||
|
||||
[DataRow(false)]
|
||||
[DataRow(true)]
|
||||
[DataTestMethod]
|
||||
[TestMethod]
|
||||
internal async Task DoesntSplitOnEscapeCharacter(bool isAccountLimited) {
|
||||
int maxMessageBytes = isAccountLimited ? MaxMessageBytesForLimitedAccounts : MaxMessageBytesForUnlimitedAccounts;
|
||||
int longLineLength = maxMessageBytes - ReservedContinuationMessageBytes;
|
||||
@@ -121,9 +134,9 @@ internal sealed class SteamChatMessage {
|
||||
string longLine = new('a', longLineLength - 1);
|
||||
string message = $"{longLine}[";
|
||||
|
||||
List<string> output = await GetMessageParts(message, isAccountLimited: isAccountLimited).ToListAsync().ConfigureAwait(false);
|
||||
List<string> output = await GetMessageParts(message, isAccountLimited: isAccountLimited).ToListAsync(CancellationToken).ConfigureAwait(false);
|
||||
|
||||
Assert.AreEqual(2, output.Count);
|
||||
Assert.HasCount(2, output);
|
||||
|
||||
Assert.AreEqual($"{longLine}{ContinuationCharacter}", output[0]);
|
||||
Assert.AreEqual($@"{ContinuationCharacter}\[", output[1]);
|
||||
@@ -133,9 +146,9 @@ internal sealed class SteamChatMessage {
|
||||
internal async Task NoNeedForAnySplittingWithNewlines() {
|
||||
string message = $"abcdef{Environment.NewLine}ghijkl{Environment.NewLine}mnopqr";
|
||||
|
||||
List<string> output = await GetMessageParts(message).ToListAsync().ConfigureAwait(false);
|
||||
List<string> output = await GetMessageParts(message).ToListAsync(CancellationToken).ConfigureAwait(false);
|
||||
|
||||
Assert.AreEqual(1, output.Count);
|
||||
Assert.HasCount(1, output);
|
||||
Assert.AreEqual(message, output.First());
|
||||
}
|
||||
|
||||
@@ -143,23 +156,23 @@ internal sealed class SteamChatMessage {
|
||||
internal async Task NoNeedForAnySplittingWithoutNewlines() {
|
||||
const string message = "abcdef";
|
||||
|
||||
List<string> output = await GetMessageParts(message).ToListAsync().ConfigureAwait(false);
|
||||
List<string> output = await GetMessageParts(message).ToListAsync(CancellationToken).ConfigureAwait(false);
|
||||
|
||||
Assert.AreEqual(1, output.Count);
|
||||
Assert.HasCount(1, output);
|
||||
Assert.AreEqual(message, output.First());
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
internal void ParagraphCharacterSizeIsLessOrEqualToContinuationCharacterSize() => Assert.IsTrue(ContinuationCharacterBytes >= Encoding.UTF8.GetByteCount(ParagraphCharacter.ToString()));
|
||||
internal void ParagraphCharacterSizeIsLessOrEqualToContinuationCharacterSize() => Assert.IsGreaterThanOrEqualTo(Encoding.UTF8.GetByteCount(ParagraphCharacter.ToString()), ContinuationCharacterBytes);
|
||||
|
||||
[TestMethod]
|
||||
internal async Task ProperlyEscapesCharacters() {
|
||||
const string message = @"[b]bold[/b] \n";
|
||||
const string escapedMessage = @"\[b]bold\[/b] \\n";
|
||||
|
||||
List<string> output = await GetMessageParts(message).ToListAsync().ConfigureAwait(false);
|
||||
List<string> output = await GetMessageParts(message).ToListAsync(CancellationToken).ConfigureAwait(false);
|
||||
|
||||
Assert.AreEqual(1, output.Count);
|
||||
Assert.HasCount(1, output);
|
||||
Assert.AreEqual(escapedMessage, output.First());
|
||||
}
|
||||
|
||||
@@ -170,15 +183,15 @@ internal sealed class SteamChatMessage {
|
||||
|
||||
const string message = "asdf";
|
||||
|
||||
List<string> output = await GetMessageParts(message, prefix).ToListAsync().ConfigureAwait(false);
|
||||
List<string> output = await GetMessageParts(message, prefix).ToListAsync(CancellationToken).ConfigureAwait(false);
|
||||
|
||||
Assert.AreEqual(1, output.Count);
|
||||
Assert.HasCount(1, output);
|
||||
Assert.AreEqual($"{escapedPrefix}{message}", output.First());
|
||||
}
|
||||
|
||||
[DataRow(false)]
|
||||
[DataRow(true)]
|
||||
[DataTestMethod]
|
||||
[TestMethod]
|
||||
internal async Task ProperlySplitsLongSingleLine(bool isAccountLimited) {
|
||||
int maxMessageBytes = isAccountLimited ? MaxMessageBytesForLimitedAccounts : MaxMessageBytesForUnlimitedAccounts;
|
||||
int longLineLength = maxMessageBytes - ReservedContinuationMessageBytes;
|
||||
@@ -186,9 +199,9 @@ internal sealed class SteamChatMessage {
|
||||
string longLine = new('a', longLineLength);
|
||||
string message = $"{longLine}{longLine}{longLine}{longLine}";
|
||||
|
||||
List<string> output = await GetMessageParts(message, isAccountLimited: isAccountLimited).ToListAsync().ConfigureAwait(false);
|
||||
List<string> output = await GetMessageParts(message, isAccountLimited: isAccountLimited).ToListAsync(CancellationToken).ConfigureAwait(false);
|
||||
|
||||
Assert.AreEqual(4, output.Count);
|
||||
Assert.HasCount(4, output);
|
||||
|
||||
Assert.AreEqual($"{longLine}{ContinuationCharacter}", output[0]);
|
||||
Assert.AreEqual($"{ContinuationCharacter}{longLine}{ContinuationCharacter}", output[1]);
|
||||
@@ -260,9 +273,9 @@ internal sealed class SteamChatMessage {
|
||||
<ASF> 1/1 ботов уже имеют игру app/304930 | Unturned.
|
||||
""";
|
||||
|
||||
List<string> output = await GetMessageParts(message, prefix).ToListAsync().ConfigureAwait(false);
|
||||
List<string> output = await GetMessageParts(message, prefix).ToListAsync(CancellationToken).ConfigureAwait(false);
|
||||
|
||||
Assert.AreEqual(2, output.Count);
|
||||
Assert.HasCount(2, output);
|
||||
|
||||
foreach (string messagePart in output) {
|
||||
if ((messagePart.Length <= prefix.Length) || !messagePart.StartsWith(prefix, StringComparison.Ordinal)) {
|
||||
@@ -285,7 +298,7 @@ internal sealed class SteamChatMessage {
|
||||
|
||||
[DataRow(false)]
|
||||
[DataRow(true)]
|
||||
[DataTestMethod]
|
||||
[TestMethod]
|
||||
internal async Task SplitsOnNewlinesWithParagraphCharacter(bool isAccountLimited) {
|
||||
int maxMessageBytes = isAccountLimited ? MaxMessageBytesForLimitedAccounts : MaxMessageBytesForUnlimitedAccounts;
|
||||
|
||||
@@ -304,9 +317,9 @@ internal sealed class SteamChatMessage {
|
||||
string newlinePart = newlinePartBuilder.ToString();
|
||||
string message = $"{newlinePart}{Environment.NewLine}{newlinePart}{Environment.NewLine}{newlinePart}{Environment.NewLine}{newlinePart}";
|
||||
|
||||
List<string> output = await GetMessageParts(message, isAccountLimited: isAccountLimited).ToListAsync().ConfigureAwait(false);
|
||||
List<string> output = await GetMessageParts(message, isAccountLimited: isAccountLimited).ToListAsync(CancellationToken).ConfigureAwait(false);
|
||||
|
||||
Assert.AreEqual(4, output.Count);
|
||||
Assert.HasCount(4, output);
|
||||
|
||||
Assert.AreEqual($"{newlinePart}{ParagraphCharacter}", output[0]);
|
||||
Assert.AreEqual($"{newlinePart}{ParagraphCharacter}", output[1]);
|
||||
@@ -320,7 +333,7 @@ internal sealed class SteamChatMessage {
|
||||
|
||||
const string message = "asdf";
|
||||
|
||||
await Assert.ThrowsExceptionAsync<ArgumentOutOfRangeException>(async () => await GetMessageParts(message, prefix).ToListAsync().ConfigureAwait(false)).ConfigureAwait(false);
|
||||
await Assert.ThrowsExactlyAsync<ArgumentOutOfRangeException>(async () => await GetMessageParts(message, prefix).ToListAsync(CancellationToken).ConfigureAwait(false)).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
@@ -329,7 +342,7 @@ internal sealed class SteamChatMessage {
|
||||
|
||||
const string message = "asdf";
|
||||
|
||||
await Assert.ThrowsExceptionAsync<ArgumentOutOfRangeException>(async () => await GetMessageParts(message, prefix).ToListAsync().ConfigureAwait(false)).ConfigureAwait(false);
|
||||
await Assert.ThrowsExactlyAsync<ArgumentOutOfRangeException>(async () => await GetMessageParts(message, prefix).ToListAsync(CancellationToken).ConfigureAwait(false)).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
#pragma warning restore CA1812 // False positive, the class is used during MSTest
|
||||
|
||||
@@ -45,6 +45,7 @@
|
||||
<s:String x:Key="/Default/CodeInspection/Highlighting/InspectionSeverities/=ArrangeTrailingCommaInSinglelineLists/@EntryIndexedValue">SUGGESTION</s:String>
|
||||
<s:String x:Key="/Default/CodeInspection/Highlighting/InspectionSeverities/=ArrangeTypeMemberModifiers/@EntryIndexedValue">SUGGESTION</s:String>
|
||||
<s:String x:Key="/Default/CodeInspection/Highlighting/InspectionSeverities/=ArrangeTypeModifiers/@EntryIndexedValue">SUGGESTION</s:String>
|
||||
<s:String x:Key="/Default/CodeInspection/Highlighting/InspectionSeverities/=AsyncVoidEventHandlerMethod/@EntryIndexedValue">HINT</s:String>
|
||||
<s:String x:Key="/Default/CodeInspection/Highlighting/InspectionSeverities/=AsyncVoidMethod/@EntryIndexedValue">HINT</s:String>
|
||||
<s:String x:Key="/Default/CodeInspection/Highlighting/InspectionSeverities/=BadAttributeBracketsSpaces/@EntryIndexedValue">WARNING</s:String>
|
||||
<s:String x:Key="/Default/CodeInspection/Highlighting/InspectionSeverities/=BadBracesSpaces/@EntryIndexedValue">WARNING</s:String>
|
||||
@@ -187,6 +188,9 @@
|
||||
<s:String x:Key="/Default/CodeInspection/Highlighting/InspectionSeverities/=ConvertToConstant_002ELocal/@EntryIndexedValue">SUGGESTION</s:String>
|
||||
<s:String x:Key="/Default/CodeInspection/Highlighting/InspectionSeverities/=ConvertToExpressionBodyWhenPossible/@EntryIndexedValue">SUGGESTION</s:String>
|
||||
<s:String x:Key="/Default/CodeInspection/Highlighting/InspectionSeverities/=ConvertToLambdaExpressionWhenPossible/@EntryIndexedValue">SUGGESTION</s:String>
|
||||
<s:String x:Key="/Default/CodeInspection/Highlighting/InspectionSeverities/=DuplicatedChainedIfBodies/@EntryIndexedValue">SUGGESTION</s:String>
|
||||
<s:String x:Key="/Default/CodeInspection/Highlighting/InspectionSeverities/=DuplicatedSequentialIfBodies/@EntryIndexedValue">SUGGESTION</s:String>
|
||||
<s:String x:Key="/Default/CodeInspection/Highlighting/InspectionSeverities/=DuplicatedSwitchExpressionArms/@EntryIndexedValue">SUGGESTION</s:String>
|
||||
<s:String x:Key="/Default/CodeInspection/Highlighting/InspectionSeverities/=DuplicatedSwitchSectionBodies/@EntryIndexedValue">SUGGESTION</s:String>
|
||||
|
||||
<s:String x:Key="/Default/CodeInspection/Highlighting/InspectionSeverities/=DuplicateResource/@EntryIndexedValue">DO_NOT_SHOW</s:String>
|
||||
@@ -239,6 +243,7 @@
|
||||
<s:String x:Key="/Default/CodeInspection/Highlighting/InspectionSeverities/=MissingSpace/@EntryIndexedValue">WARNING</s:String>
|
||||
<s:String x:Key="/Default/CodeInspection/Highlighting/InspectionSeverities/=MoveLocalFunctionAfterJumpStatement/@EntryIndexedValue">SUGGESTION</s:String>
|
||||
<s:String x:Key="/Default/CodeInspection/Highlighting/InspectionSeverities/=MoveToExistingPositionalDeconstructionPattern/@EntryIndexedValue">SUGGESTION</s:String>
|
||||
<s:String x:Key="/Default/CodeInspection/Highlighting/InspectionSeverities/=MoveToExtensionBlock/@EntryIndexedValue">SUGGESTION</s:String>
|
||||
<s:String x:Key="/Default/CodeInspection/Highlighting/InspectionSeverities/=MultipleSpaces/@EntryIndexedValue">WARNING</s:String>
|
||||
<s:String x:Key="/Default/CodeInspection/Highlighting/InspectionSeverities/=MultipleStatementsOnOneLine/@EntryIndexedValue">WARNING</s:String>
|
||||
<s:String x:Key="/Default/CodeInspection/Highlighting/InspectionSeverities/=MultipleTypeMembersOnOneLine/@EntryIndexedValue">WARNING</s:String>
|
||||
@@ -256,6 +261,7 @@
|
||||
<s:String x:Key="/Default/CodeInspection/Highlighting/InspectionSeverities/=PatternAlwaysMatches/@EntryIndexedValue">WARNING</s:String>
|
||||
<s:String x:Key="/Default/CodeInspection/Highlighting/InspectionSeverities/=PatternAlwaysOfType/@EntryIndexedValue">WARNING</s:String>
|
||||
<s:String x:Key="/Default/CodeInspection/Highlighting/InspectionSeverities/=PlaceAssignmentExpressionIntoBlock/@EntryIndexedValue">DO_NOT_SHOW</s:String>
|
||||
<s:String x:Key="/Default/CodeInspection/Highlighting/InspectionSeverities/=PreferExplicitlyProvidedTupleComponentName/@EntryIndexedValue">SUGGESTION</s:String>
|
||||
<s:String x:Key="/Default/CodeInspection/Highlighting/InspectionSeverities/=PrimaryConstructorParameterCaptureDisallowed/@EntryIndexedValue">WARNING</s:String>
|
||||
<s:String x:Key="/Default/CodeInspection/Highlighting/InspectionSeverities/=PropertyNotResolved/@EntryIndexedValue">WARNING</s:String>
|
||||
<s:String x:Key="/Default/CodeInspection/Highlighting/InspectionSeverities/=RawStringCanBeSimplified/@EntryIndexedValue">SUGGESTION</s:String>
|
||||
|
||||
@@ -4,22 +4,25 @@
|
||||
<OutputType>Exe</OutputType>
|
||||
</PropertyGroup>
|
||||
|
||||
<!-- TODO: It seems CA1515 is no longer respected inside editorconfig, might be possible to remove later -->
|
||||
<PropertyGroup>
|
||||
<NoWarn>$(NoWarn),CA1515</NoWarn>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="AngleSharp.XPath" />
|
||||
<PackageReference Include="AngleSharp" />
|
||||
<PackageReference Include="CryptSharpStandard" />
|
||||
<PackageReference Include="Humanizer" />
|
||||
<PackageReference Include="JetBrains.Annotations" PrivateAssets="all" />
|
||||
<PackageReference Include="JetBrains.Annotations.Sources" PrivateAssets="all" />
|
||||
<PackageReference Include="Markdig.Signed" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.OpenApi" />
|
||||
<PackageReference Include="Microsoft.CodeAnalysis.ResxSourceGenerator" PrivateAssets="all" />
|
||||
<PackageReference Include="Microsoft.IdentityModel.JsonWebTokens" />
|
||||
<PackageReference Include="Nito.AsyncEx.Coordination" />
|
||||
<PackageReference Include="NLog.Web.AspNetCore" />
|
||||
<PackageReference Include="Scalar.AspNetCore" />
|
||||
<PackageReference Include="SteamKit2" />
|
||||
<PackageReference Include="Swashbuckle.AspNetCore" />
|
||||
<PackageReference Include="Swashbuckle.AspNetCore.Annotations" />
|
||||
<PackageReference Include="System.Composition" />
|
||||
<PackageReference Include="System.Linq.Async" />
|
||||
<PackageReference Include="System.Security.Cryptography.ProtectedData" />
|
||||
</ItemGroup>
|
||||
|
||||
|
||||
@@ -100,9 +100,7 @@ public sealed class ConcurrentHashSet<T> : IReadOnlySet<T>, ISet<T> where T : no
|
||||
public void ExceptWith(IEnumerable<T> other) {
|
||||
ArgumentNullException.ThrowIfNull(other);
|
||||
|
||||
foreach (T item in other) {
|
||||
Remove(item);
|
||||
}
|
||||
RemoveRange(other);
|
||||
}
|
||||
|
||||
[MustDisposeResource]
|
||||
@@ -113,8 +111,14 @@ public sealed class ConcurrentHashSet<T> : IReadOnlySet<T>, ISet<T> where T : no
|
||||
|
||||
IReadOnlySet<T> otherSet = other as IReadOnlySet<T> ?? other.ToHashSet();
|
||||
|
||||
foreach (T item in this.Where(item => !otherSet.Contains(item))) {
|
||||
Remove(item);
|
||||
bool modified = false;
|
||||
|
||||
foreach (T _ in this.Where(item => !otherSet.Contains(item) && BackingCollection.TryRemove(item, out _))) {
|
||||
modified = true;
|
||||
}
|
||||
|
||||
if (modified) {
|
||||
OnModified?.Invoke(this, EventArgs.Empty);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -182,24 +186,24 @@ public sealed class ConcurrentHashSet<T> : IReadOnlySet<T>, ISet<T> where T : no
|
||||
ArgumentNullException.ThrowIfNull(other);
|
||||
|
||||
IReadOnlySet<T> otherSet = other as IReadOnlySet<T> ?? other.ToHashSet();
|
||||
HashSet<T> removed = [];
|
||||
|
||||
foreach (T item in otherSet.Where(Contains)) {
|
||||
removed.Add(item);
|
||||
Remove(item);
|
||||
HashSet<T> removed = otherSet.Where(item => BackingCollection.TryRemove(item, out _)).ToHashSet();
|
||||
|
||||
bool modified = removed.Count > 0;
|
||||
|
||||
foreach (T _ in otherSet.Where(item => !removed.Contains(item) && BackingCollection.TryAdd(item, true))) {
|
||||
modified = true;
|
||||
}
|
||||
|
||||
foreach (T item in otherSet.Where(item => !removed.Contains(item))) {
|
||||
Add(item);
|
||||
if (modified) {
|
||||
OnModified?.Invoke(this, EventArgs.Empty);
|
||||
}
|
||||
}
|
||||
|
||||
public void UnionWith(IEnumerable<T> other) {
|
||||
ArgumentNullException.ThrowIfNull(other);
|
||||
|
||||
foreach (T otherElement in other) {
|
||||
Add(otherElement);
|
||||
}
|
||||
AddRange(other);
|
||||
}
|
||||
|
||||
void ICollection<T>.Add(T item) {
|
||||
@@ -215,44 +219,66 @@ public sealed class ConcurrentHashSet<T> : IReadOnlySet<T>, ISet<T> where T : no
|
||||
public bool AddRange(IEnumerable<T> items) {
|
||||
ArgumentNullException.ThrowIfNull(items);
|
||||
|
||||
bool result = false;
|
||||
bool modified = false;
|
||||
|
||||
foreach (T _ in items.Where(Add)) {
|
||||
result = true;
|
||||
foreach (T _ in items.Where(item => BackingCollection.TryAdd(item, true))) {
|
||||
modified = true;
|
||||
}
|
||||
|
||||
return result;
|
||||
if (modified) {
|
||||
OnModified?.Invoke(this, EventArgs.Empty);
|
||||
}
|
||||
|
||||
return modified;
|
||||
}
|
||||
|
||||
[PublicAPI]
|
||||
public bool RemoveRange(IEnumerable<T> items) {
|
||||
ArgumentNullException.ThrowIfNull(items);
|
||||
|
||||
bool result = false;
|
||||
bool modified = false;
|
||||
|
||||
foreach (T _ in items.Where(Remove)) {
|
||||
result = true;
|
||||
foreach (T _ in items.Where(item => BackingCollection.TryRemove(item, out _))) {
|
||||
modified = true;
|
||||
}
|
||||
|
||||
return result;
|
||||
if (modified) {
|
||||
OnModified?.Invoke(this, EventArgs.Empty);
|
||||
}
|
||||
|
||||
return modified;
|
||||
}
|
||||
|
||||
[PublicAPI]
|
||||
public int RemoveWhere(Predicate<T> match) {
|
||||
ArgumentNullException.ThrowIfNull(match);
|
||||
|
||||
return BackingCollection.Keys.Where(match.Invoke).Count(key => BackingCollection.TryRemove(key, out _));
|
||||
int count = BackingCollection.Keys.Where(match.Invoke).Count(key => BackingCollection.TryRemove(key, out _));
|
||||
|
||||
if (count > 0) {
|
||||
OnModified?.Invoke(this, EventArgs.Empty);
|
||||
}
|
||||
|
||||
return count;
|
||||
}
|
||||
|
||||
[PublicAPI]
|
||||
public bool ReplaceIfNeededWith(IReadOnlyCollection<T> other) {
|
||||
public bool ReplaceIfNeededWith(IEnumerable<T> other) {
|
||||
ArgumentNullException.ThrowIfNull(other);
|
||||
|
||||
if (SetEquals(other)) {
|
||||
ICollection<T> otherCollection = other as ICollection<T> ?? other.ToHashSet();
|
||||
|
||||
if (SetEquals(otherCollection)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
ReplaceWith(other);
|
||||
BackingCollection.Clear();
|
||||
|
||||
foreach (T item in otherCollection) {
|
||||
BackingCollection.TryAdd(item, true);
|
||||
}
|
||||
|
||||
OnModified?.Invoke(this, EventArgs.Empty);
|
||||
|
||||
return true;
|
||||
}
|
||||
@@ -261,7 +287,6 @@ public sealed class ConcurrentHashSet<T> : IReadOnlySet<T>, ISet<T> where T : no
|
||||
public void ReplaceWith(IEnumerable<T> other) {
|
||||
ArgumentNullException.ThrowIfNull(other);
|
||||
|
||||
Clear();
|
||||
UnionWith(other);
|
||||
ReplaceIfNeededWith(other);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -24,6 +24,7 @@
|
||||
using System;
|
||||
using System.Collections;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text.Json.Serialization;
|
||||
using JetBrains.Annotations;
|
||||
using Nito.AsyncEx;
|
||||
@@ -60,6 +61,12 @@ public sealed class ConcurrentList<T> : IList<T>, IReadOnlyList<T> {
|
||||
ArgumentOutOfRangeException.ThrowIfNegative(index);
|
||||
|
||||
using (Lock.WriterLock()) {
|
||||
T oldValue = BackingCollection[index];
|
||||
|
||||
if (EqualityComparer<T>.Default.Equals(oldValue, value)) {
|
||||
return;
|
||||
}
|
||||
|
||||
BackingCollection[index] = value;
|
||||
}
|
||||
|
||||
@@ -86,6 +93,10 @@ public sealed class ConcurrentList<T> : IList<T>, IReadOnlyList<T> {
|
||||
|
||||
public void Clear() {
|
||||
using (Lock.WriterLock()) {
|
||||
if (BackingCollection.Count == 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
BackingCollection.Clear();
|
||||
}
|
||||
|
||||
@@ -155,9 +166,15 @@ public sealed class ConcurrentList<T> : IList<T>, IReadOnlyList<T> {
|
||||
public void ReplaceWith(IEnumerable<T> collection) {
|
||||
ArgumentNullException.ThrowIfNull(collection);
|
||||
|
||||
ICollection<T> newCollection = collection as ICollection<T> ?? collection.ToList();
|
||||
|
||||
using (Lock.WriterLock()) {
|
||||
if (BackingCollection.SequenceEqual(newCollection)) {
|
||||
return;
|
||||
}
|
||||
|
||||
BackingCollection.Clear();
|
||||
BackingCollection.AddRange(collection);
|
||||
BackingCollection.AddRange(newCollection);
|
||||
}
|
||||
|
||||
OnModified?.Invoke(this, EventArgs.Empty);
|
||||
|
||||
@@ -65,6 +65,7 @@ public sealed class ObservableConcurrentDictionary<TKey, TValue> : IDictionary<T
|
||||
}
|
||||
|
||||
BackingDictionary[key] = value;
|
||||
|
||||
OnModified?.Invoke(this, EventArgs.Empty);
|
||||
}
|
||||
}
|
||||
@@ -111,6 +112,7 @@ public sealed class ObservableConcurrentDictionary<TKey, TValue> : IDictionary<T
|
||||
}
|
||||
|
||||
BackingDictionary.Clear();
|
||||
|
||||
OnModified?.Invoke(this, EventArgs.Empty);
|
||||
}
|
||||
|
||||
|
||||
@@ -83,7 +83,7 @@ public static class ASF {
|
||||
internal static ICrossProcessSemaphore? RateLimitingSemaphore { get; private set; }
|
||||
internal static FrozenDictionary<Uri, (ICrossProcessSemaphore RateLimitingSemaphore, SemaphoreSlim OpenConnectionsSemaphore)>? WebLimitingSemaphores { get; private set; }
|
||||
|
||||
private static readonly FrozenSet<string> AssembliesNeededBeforeUpdate = new HashSet<string>(1, StringComparer.Ordinal) { "System.IO.Pipes" }.ToFrozenSet(StringComparer.Ordinal);
|
||||
private static readonly FrozenSet<string> AssembliesNeededBeforeUpdate = FrozenSet.Create(StringComparer.Ordinal, "System.IO.Pipes");
|
||||
private static readonly SemaphoreSlim UpdateSemaphore = new(1, 1);
|
||||
|
||||
private static Timer? AutoUpdatesTimer;
|
||||
@@ -355,8 +355,8 @@ public static class ASF {
|
||||
}
|
||||
|
||||
ArchiLogger.LogGenericInfo(Strings.IPCConfigChanged);
|
||||
await ArchiKestrel.Stop().ConfigureAwait(false);
|
||||
await ArchiKestrel.Start().ConfigureAwait(false);
|
||||
|
||||
await ArchiKestrel.Restart().ConfigureAwait(false);
|
||||
}
|
||||
|
||||
private static async Task OnChangedFile(string name, string fullPath) {
|
||||
@@ -773,13 +773,7 @@ public static class ASF {
|
||||
|
||||
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)) {
|
||||
if ((releaseResponse == null) || string.IsNullOrEmpty(releaseResponse.Tag)) {
|
||||
ArchiLogger.LogGenericWarning(Strings.ErrorUpdateCheckFailed);
|
||||
|
||||
return (false, null);
|
||||
@@ -849,8 +843,7 @@ public static class ASF {
|
||||
BinaryResponse? response;
|
||||
|
||||
try {
|
||||
// ReSharper disable once MethodSupportsCancellation - the token initialized above is not meant to be passed here
|
||||
response = await WebBrowser.UrlGetToBinary(binaryAsset.DownloadURL, progressReporter: progressReporter).ConfigureAwait(false);
|
||||
response = await WebBrowser.UrlGetToBinary(binaryAsset.DownloadURL, progressReporter: progressReporter, cancellationToken: CancellationToken.None).ConfigureAwait(false);
|
||||
} finally {
|
||||
progressReporter.ProgressChanged -= onProgressChanged;
|
||||
}
|
||||
@@ -894,10 +887,12 @@ public static class ASF {
|
||||
MemoryStream memoryStream = new(responseBytes);
|
||||
|
||||
await using (memoryStream.ConfigureAwait(false)) {
|
||||
using ZipArchive zipArchive = new(memoryStream);
|
||||
ZipArchive zipArchive = new(memoryStream);
|
||||
|
||||
if (!await UpdateFromArchive(newVersion, channel.Value, updateOverride, forced, zipArchive).ConfigureAwait(false)) {
|
||||
ArchiLogger.LogGenericError(Strings.WarningFailed);
|
||||
await using (zipArchive.ConfigureAwait(false)) {
|
||||
if (!await UpdateFromArchive(newVersion, channel.Value, updateOverride, forced, zipArchive).ConfigureAwait(false)) {
|
||||
ArchiLogger.LogGenericError(Strings.WarningFailed);
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (Exception e) {
|
||||
|
||||
@@ -114,7 +114,7 @@ internal static class ArchiNet {
|
||||
return null;
|
||||
}
|
||||
|
||||
IAttr? paramsNode = challengeResponse.Content.SelectSingleNode<IAttr>("//input[@name='openidparams']/@value");
|
||||
IElement? paramsNode = challengeResponse.Content.QuerySelector("input[name='openidparams'][value]");
|
||||
|
||||
if (paramsNode == null) {
|
||||
ASF.ArchiLogger.LogNullError(paramsNode);
|
||||
@@ -122,7 +122,7 @@ internal static class ArchiNet {
|
||||
return null;
|
||||
}
|
||||
|
||||
string paramsValue = paramsNode.Value;
|
||||
string? paramsValue = paramsNode.GetAttribute("value");
|
||||
|
||||
if (string.IsNullOrEmpty(paramsValue)) {
|
||||
ASF.ArchiLogger.LogNullError(paramsValue);
|
||||
@@ -130,7 +130,7 @@ internal static class ArchiNet {
|
||||
return null;
|
||||
}
|
||||
|
||||
IAttr? nonceNode = challengeResponse.Content.SelectSingleNode<IAttr>("//input[@name='nonce']/@value");
|
||||
IElement? nonceNode = challengeResponse.Content.QuerySelector("input[name='nonce'][value]");
|
||||
|
||||
if (nonceNode == null) {
|
||||
ASF.ArchiLogger.LogNullError(nonceNode);
|
||||
@@ -138,7 +138,7 @@ internal static class ArchiNet {
|
||||
return null;
|
||||
}
|
||||
|
||||
string nonceValue = nonceNode.Value;
|
||||
string? nonceValue = nonceNode.GetAttribute("value");
|
||||
|
||||
if (string.IsNullOrEmpty(nonceValue)) {
|
||||
ASF.ArchiLogger.LogNullError(nonceValue);
|
||||
@@ -183,7 +183,7 @@ internal static class ArchiNet {
|
||||
|
||||
private static async Task<(bool Success, IReadOnlyCollection<ulong>? Result)> ResolveCachedBadBots(CancellationToken cancellationToken = default) {
|
||||
if (ASF.GlobalDatabase == null) {
|
||||
throw new InvalidOperationException(nameof(ASF.WebBrowser));
|
||||
throw new InvalidOperationException(nameof(ASF.GlobalDatabase));
|
||||
}
|
||||
|
||||
if (ASF.WebBrowser == null) {
|
||||
|
||||
@@ -304,6 +304,11 @@ internal static class OS {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!consoleMode.HasFlag(NativeMethods.EConsoleMode.EnableQuickEditMode)) {
|
||||
// Quick edit mode already disabled
|
||||
return;
|
||||
}
|
||||
|
||||
consoleMode &= ~NativeMethods.EConsoleMode.EnableQuickEditMode;
|
||||
|
||||
if (!NativeMethods.SetConsoleMode(consoleHandle, consoleMode)) {
|
||||
|
||||
@@ -35,8 +35,6 @@ using System.Resources;
|
||||
using System.Security.Cryptography;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using AngleSharp.Dom;
|
||||
using AngleSharp.XPath;
|
||||
using ArchiSteamFarm.Localization;
|
||||
using ArchiSteamFarm.NLog;
|
||||
using ArchiSteamFarm.Storage;
|
||||
@@ -53,7 +51,7 @@ public static class Utilities {
|
||||
private const byte TimeoutForLongRunningTasksInSeconds = 60;
|
||||
private const uint UnauthorizedAccessHResult = 0x80070005;
|
||||
|
||||
private static readonly FrozenSet<char> DirectorySeparators = new HashSet<char>(2) { Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar }.ToFrozenSet();
|
||||
private static readonly FrozenSet<char> DirectorySeparators = [Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar];
|
||||
|
||||
[PublicAPI]
|
||||
public static IEnumerable<T> AsLinqThreadSafeEnumerable<T>(this ICollection<T> collection) {
|
||||
@@ -171,18 +169,6 @@ public static class Utilities {
|
||||
}
|
||||
}
|
||||
|
||||
[PublicAPI]
|
||||
public static bool IsClientErrorCode(this HttpStatusCode statusCode) => statusCode is >= HttpStatusCode.BadRequest and < HttpStatusCode.InternalServerError;
|
||||
|
||||
[PublicAPI]
|
||||
public static bool IsRedirectionCode(this HttpStatusCode statusCode) => statusCode is >= HttpStatusCode.Ambiguous and < HttpStatusCode.BadRequest;
|
||||
|
||||
[PublicAPI]
|
||||
public static bool IsServerErrorCode(this HttpStatusCode statusCode) => statusCode is >= HttpStatusCode.InternalServerError and < (HttpStatusCode) 600;
|
||||
|
||||
[PublicAPI]
|
||||
public static bool IsSuccessCode(this HttpStatusCode statusCode) => statusCode is >= HttpStatusCode.OK and < HttpStatusCode.Ambiguous;
|
||||
|
||||
[PublicAPI]
|
||||
public static bool IsValidCdKey(string key) {
|
||||
ArgumentException.ThrowIfNullOrEmpty(key);
|
||||
@@ -197,48 +183,6 @@ public static class Utilities {
|
||||
return (text.Length % 2 == 0) && text.All(Uri.IsHexDigit);
|
||||
}
|
||||
|
||||
[PublicAPI]
|
||||
public static IList<INode> SelectNodes(this IDocument document, string xpath) {
|
||||
ArgumentNullException.ThrowIfNull(document);
|
||||
|
||||
return document.Body.SelectNodes(xpath);
|
||||
}
|
||||
|
||||
[PublicAPI]
|
||||
public static IEnumerable<T> SelectNodes<T>(this IDocument document, string xpath) where T : class, INode {
|
||||
ArgumentNullException.ThrowIfNull(document);
|
||||
|
||||
return document.Body.SelectNodes(xpath).OfType<T>();
|
||||
}
|
||||
|
||||
[PublicAPI]
|
||||
public static IEnumerable<T> SelectNodes<T>(this IElement element, string xpath) where T : class, INode {
|
||||
ArgumentNullException.ThrowIfNull(element);
|
||||
|
||||
return element.SelectNodes(xpath).OfType<T>();
|
||||
}
|
||||
|
||||
[PublicAPI]
|
||||
public static INode? SelectSingleNode(this IDocument document, string xpath) {
|
||||
ArgumentNullException.ThrowIfNull(document);
|
||||
|
||||
return document.Body.SelectSingleNode(xpath);
|
||||
}
|
||||
|
||||
[PublicAPI]
|
||||
public static T? SelectSingleNode<T>(this IDocument document, string xpath) where T : class, INode {
|
||||
ArgumentNullException.ThrowIfNull(document);
|
||||
|
||||
return document.Body.SelectSingleNode(xpath) as T;
|
||||
}
|
||||
|
||||
[PublicAPI]
|
||||
public static T? SelectSingleNode<T>(this IElement element, string xpath) where T : class, INode {
|
||||
ArgumentNullException.ThrowIfNull(element);
|
||||
|
||||
return element.SelectSingleNode(xpath) as T;
|
||||
}
|
||||
|
||||
[PublicAPI]
|
||||
public static IEnumerable<T> ToEnumerable<T>(this T item) {
|
||||
yield return item;
|
||||
@@ -357,7 +301,7 @@ public static class Utilities {
|
||||
// Now extract the zip file to entirely new location, this decreases chance of corruptions if user kills the process during this stage
|
||||
string updateDirectory = Path.Combine(targetDirectory, SharedInfo.UpdateDirectoryNew);
|
||||
|
||||
zipArchive.ExtractToDirectory(updateDirectory, true);
|
||||
await zipArchive.ExtractToDirectoryAsync(updateDirectory, true).ConfigureAwait(false);
|
||||
|
||||
// Now, critical section begins, we're going to move all files from target directory to a backup directory
|
||||
string backupDirectory = Path.Combine(targetDirectory, SharedInfo.UpdateDirectoryOld);
|
||||
@@ -569,4 +513,20 @@ public static class Utilities {
|
||||
|
||||
return prefixes.Any(prefix => !string.IsNullOrEmpty(prefix) && (directory.Length > prefix.Length) && DirectorySeparators.Contains(directory[prefix.Length]) && directory.StartsWith(prefix, StringComparison.Ordinal));
|
||||
}
|
||||
|
||||
#pragma warning disable CA1034 // False positive, there's no other way we can declare this block
|
||||
extension(HttpStatusCode statusCode) {
|
||||
[PublicAPI]
|
||||
public bool IsClientErrorCode() => statusCode is >= HttpStatusCode.BadRequest and < HttpStatusCode.InternalServerError;
|
||||
|
||||
[PublicAPI]
|
||||
public bool IsRedirectionCode() => statusCode is >= HttpStatusCode.Ambiguous and < HttpStatusCode.BadRequest;
|
||||
|
||||
[PublicAPI]
|
||||
public bool IsServerErrorCode() => statusCode is >= HttpStatusCode.InternalServerError and < (HttpStatusCode) 600;
|
||||
|
||||
[PublicAPI]
|
||||
public bool IsSuccessCode() => statusCode is >= HttpStatusCode.OK and < HttpStatusCode.Ambiguous;
|
||||
}
|
||||
#pragma warning restore CA1034 // False positive, there's no other way we can declare this block
|
||||
}
|
||||
|
||||
@@ -28,6 +28,7 @@ using System.Security.AccessControl;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using ArchiSteamFarm.Core;
|
||||
using ArchiSteamFarm.Localization;
|
||||
|
||||
namespace ArchiSteamFarm.Helpers;
|
||||
|
||||
@@ -43,8 +44,6 @@ internal sealed class CrossProcessFileBasedSemaphore : IAsyncDisposable, ICrossP
|
||||
ArgumentException.ThrowIfNullOrEmpty(name);
|
||||
|
||||
FilePath = Path.Combine(Path.GetTempPath(), SharedInfo.ASF, name);
|
||||
|
||||
EnsureFileExists();
|
||||
}
|
||||
|
||||
public void Dispose() {
|
||||
@@ -86,6 +85,14 @@ internal sealed class CrossProcessFileBasedSemaphore : IAsyncDisposable, ICrossP
|
||||
|
||||
try {
|
||||
while (true) {
|
||||
if (!await EnsureFileExists().ConfigureAwait(false)) {
|
||||
ASF.ArchiLogger.LogGenericError(Strings.FormatWarningFailedWithError(nameof(EnsureFileExists)));
|
||||
|
||||
await Task.Delay(SpinLockDelay * 25, cancellationToken).ConfigureAwait(false);
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
try {
|
||||
// ReSharper disable once SuspiciousLockOverSynchronizationPrimitive - this is not a mistake, we need extra synchronization, and we can re-use the semaphore object for that
|
||||
lock (LocalSemaphore) {
|
||||
@@ -93,14 +100,16 @@ internal sealed class CrossProcessFileBasedSemaphore : IAsyncDisposable, ICrossP
|
||||
throw new InvalidOperationException(nameof(FileLock));
|
||||
}
|
||||
|
||||
EnsureFileExists();
|
||||
|
||||
FileLock = new FileStream(FilePath, FileMode.OpenOrCreate, FileAccess.Read, FileShare.None);
|
||||
FileLock = new FileStream(FilePath, FileMode.Open, FileAccess.Read, FileShare.None);
|
||||
success = true;
|
||||
|
||||
return;
|
||||
}
|
||||
} catch (IOException) {
|
||||
} catch (FileNotFoundException) {
|
||||
throw;
|
||||
} catch (IOException e) {
|
||||
ASF.ArchiLogger.LogGenericDebuggingException(e);
|
||||
|
||||
await Task.Delay(SpinLockDelay, cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
@@ -125,13 +134,28 @@ internal sealed class CrossProcessFileBasedSemaphore : IAsyncDisposable, ICrossP
|
||||
try {
|
||||
stopwatch.Stop();
|
||||
|
||||
if (stopwatch.ElapsedMilliseconds >= millisecondsTimeout) {
|
||||
if (stopwatch.ElapsedMilliseconds > millisecondsTimeout) {
|
||||
return false;
|
||||
}
|
||||
|
||||
millisecondsTimeout -= (int) stopwatch.ElapsedMilliseconds;
|
||||
|
||||
while (true) {
|
||||
if (!await EnsureFileExists().ConfigureAwait(false)) {
|
||||
ASF.ArchiLogger.LogGenericError(Strings.FormatWarningFailedWithError(nameof(EnsureFileExists)));
|
||||
|
||||
if (millisecondsTimeout <= 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
int sleep = Math.Min(millisecondsTimeout, SpinLockDelay * 25);
|
||||
|
||||
await Task.Delay(sleep, cancellationToken).ConfigureAwait(false);
|
||||
millisecondsTimeout -= sleep;
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
try {
|
||||
// ReSharper disable once SuspiciousLockOverSynchronizationPrimitive - this is not a mistake, we need extra synchronization, and we can re-use the semaphore object for that
|
||||
lock (LocalSemaphore) {
|
||||
@@ -139,20 +163,24 @@ internal sealed class CrossProcessFileBasedSemaphore : IAsyncDisposable, ICrossP
|
||||
throw new InvalidOperationException(nameof(FileLock));
|
||||
}
|
||||
|
||||
EnsureFileExists();
|
||||
|
||||
FileLock = new FileStream(FilePath, FileMode.OpenOrCreate, FileAccess.Read, FileShare.None);
|
||||
FileLock = new FileStream(FilePath, FileMode.Open, FileAccess.Read, FileShare.None);
|
||||
success = true;
|
||||
|
||||
return true;
|
||||
}
|
||||
} catch (IOException) {
|
||||
if (millisecondsTimeout <= SpinLockDelay) {
|
||||
} catch (FileNotFoundException) {
|
||||
throw;
|
||||
} catch (IOException e) {
|
||||
ASF.ArchiLogger.LogGenericDebuggingException(e);
|
||||
|
||||
if (millisecondsTimeout <= 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
await Task.Delay(SpinLockDelay, cancellationToken).ConfigureAwait(false);
|
||||
millisecondsTimeout -= SpinLockDelay;
|
||||
int sleep = Math.Min(millisecondsTimeout, SpinLockDelay);
|
||||
|
||||
await Task.Delay(sleep, cancellationToken).ConfigureAwait(false);
|
||||
millisecondsTimeout -= sleep;
|
||||
}
|
||||
}
|
||||
} finally {
|
||||
@@ -162,55 +190,86 @@ internal sealed class CrossProcessFileBasedSemaphore : IAsyncDisposable, ICrossP
|
||||
}
|
||||
}
|
||||
|
||||
private void EnsureFileExists() {
|
||||
if (File.Exists(FilePath)) {
|
||||
return;
|
||||
}
|
||||
private async Task<bool> EnsureFileExists() {
|
||||
for (byte i = 0; i < 2; i++) {
|
||||
if (File.Exists(FilePath)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
string? directoryPath = Path.GetDirectoryName(FilePath);
|
||||
string? directoryPath = Path.GetDirectoryName(FilePath);
|
||||
|
||||
if (string.IsNullOrEmpty(directoryPath)) {
|
||||
ASF.ArchiLogger.LogNullError(directoryPath);
|
||||
if (string.IsNullOrEmpty(directoryPath)) {
|
||||
ASF.ArchiLogger.LogNullError(directoryPath);
|
||||
|
||||
return;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!Directory.Exists(directoryPath)) {
|
||||
DirectoryInfo directoryInfo = Directory.CreateDirectory(directoryPath);
|
||||
|
||||
if (OperatingSystem.IsWindows()) {
|
||||
if (!Directory.Exists(directoryPath)) {
|
||||
try {
|
||||
DirectorySecurity directorySecurity = new(directoryPath, AccessControlSections.All);
|
||||
if (OperatingSystem.IsWindows()) {
|
||||
DirectoryInfo directoryInfo = Directory.CreateDirectory(directoryPath);
|
||||
|
||||
directoryInfo.SetAccessControl(directorySecurity);
|
||||
} catch (PrivilegeNotHeldException e) {
|
||||
// Non-critical, user might have no rights to manage the resource
|
||||
ASF.ArchiLogger.LogGenericDebuggingException(e);
|
||||
try {
|
||||
DirectorySecurity directorySecurity = new(directoryPath, AccessControlSections.All);
|
||||
|
||||
directoryInfo.SetAccessControl(directorySecurity);
|
||||
} catch (PrivilegeNotHeldException e) {
|
||||
// Non-critical, user might have no rights to manage the resource
|
||||
ASF.ArchiLogger.LogGenericDebuggingException(e);
|
||||
}
|
||||
} else if (OperatingSystem.IsFreeBSD() || OperatingSystem.IsLinux() || OperatingSystem.IsMacOS()) {
|
||||
// We require global access from all users, as other ASFs might need to put additional files in there
|
||||
Directory.CreateDirectory(directoryPath, UnixFileMode.UserRead | UnixFileMode.UserWrite | UnixFileMode.UserExecute | UnixFileMode.GroupRead | UnixFileMode.GroupWrite | UnixFileMode.GroupExecute | UnixFileMode.OtherRead | UnixFileMode.OtherWrite | UnixFileMode.OtherExecute);
|
||||
}
|
||||
} catch (IOException e) {
|
||||
ASF.ArchiLogger.LogGenericException(e);
|
||||
|
||||
return false;
|
||||
}
|
||||
} else if (OperatingSystem.IsFreeBSD() || OperatingSystem.IsLinux() || OperatingSystem.IsMacOS()) {
|
||||
directoryInfo.UnixFileMode |= UnixFileMode.UserRead | UnixFileMode.UserWrite | UnixFileMode.UserExecute | UnixFileMode.GroupRead | UnixFileMode.GroupWrite | UnixFileMode.GroupExecute | UnixFileMode.OtherRead | UnixFileMode.OtherWrite | UnixFileMode.OtherExecute;
|
||||
}
|
||||
|
||||
FileStreamOptions fileStreamOptions = new() {
|
||||
Mode = FileMode.CreateNew,
|
||||
Access = FileAccess.Write,
|
||||
Share = FileShare.None
|
||||
};
|
||||
|
||||
try {
|
||||
if (OperatingSystem.IsWindows()) {
|
||||
await new FileStream(FilePath, fileStreamOptions).DisposeAsync().ConfigureAwait(false);
|
||||
|
||||
FileInfo fileInfo = new(FilePath);
|
||||
|
||||
try {
|
||||
FileSecurity fileSecurity = new(FilePath, AccessControlSections.All);
|
||||
|
||||
fileInfo.SetAccessControl(fileSecurity);
|
||||
} catch (PrivilegeNotHeldException e) {
|
||||
// Non-critical, user might have no rights to manage the resource
|
||||
ASF.ArchiLogger.LogGenericDebuggingException(e);
|
||||
}
|
||||
} else if (OperatingSystem.IsFreeBSD() || OperatingSystem.IsLinux() || OperatingSystem.IsMacOS()) {
|
||||
// Since we only create and read the files, we don't need write/execute permissions on them from other instances
|
||||
fileStreamOptions.UnixCreateMode = UnixFileMode.UserRead | UnixFileMode.GroupRead | UnixFileMode.OtherRead;
|
||||
|
||||
await new FileStream(FilePath, fileStreamOptions).DisposeAsync().ConfigureAwait(false);
|
||||
}
|
||||
} catch (IOException e) {
|
||||
if (i == 0) {
|
||||
// Ignored, if the file was already created in the meantime by another instance, this is fine
|
||||
ASF.ArchiLogger.LogGenericDebuggingException(e);
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
// It's not fine if the same issue happened again
|
||||
ASF.ArchiLogger.LogGenericException(e);
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
new FileStream(FilePath, FileMode.CreateNew).Dispose();
|
||||
|
||||
FileInfo fileInfo = new(FilePath);
|
||||
|
||||
if (OperatingSystem.IsWindows()) {
|
||||
try {
|
||||
FileSecurity fileSecurity = new(FilePath, AccessControlSections.All);
|
||||
|
||||
fileInfo.SetAccessControl(fileSecurity);
|
||||
} catch (PrivilegeNotHeldException e) {
|
||||
// Non-critical, user might have no rights to manage the resource
|
||||
ASF.ArchiLogger.LogGenericDebuggingException(e);
|
||||
}
|
||||
} else if (OperatingSystem.IsFreeBSD() || OperatingSystem.IsLinux() || OperatingSystem.IsMacOS()) {
|
||||
fileInfo.UnixFileMode |= UnixFileMode.UserRead | UnixFileMode.UserWrite | UnixFileMode.UserExecute | UnixFileMode.GroupRead | UnixFileMode.GroupWrite | UnixFileMode.GroupExecute | UnixFileMode.OtherRead | UnixFileMode.OtherWrite | UnixFileMode.OtherExecute;
|
||||
}
|
||||
} catch (IOException) {
|
||||
// Ignored, if the file was already created in the meantime by another instance, this is fine
|
||||
}
|
||||
// It's also not fine if we failed to create the file twice in a row
|
||||
return File.Exists(FilePath);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -29,7 +29,7 @@ namespace ArchiSteamFarm.Helpers;
|
||||
|
||||
[PublicAPI]
|
||||
public interface ICrossProcessSemaphore {
|
||||
void Release();
|
||||
Task WaitAsync(CancellationToken cancellationToken = default);
|
||||
Task<bool> WaitAsync(int millisecondsTimeout, CancellationToken cancellationToken = default);
|
||||
public void Release();
|
||||
public Task WaitAsync(CancellationToken cancellationToken = default);
|
||||
public Task<bool> WaitAsync(int millisecondsTimeout, CancellationToken cancellationToken = default);
|
||||
}
|
||||
|
||||
@@ -29,18 +29,19 @@ using JetBrains.Annotations;
|
||||
namespace ArchiSteamFarm.Helpers.Json;
|
||||
|
||||
[PublicAPI]
|
||||
public sealed class BooleanNumberConverter : JsonConverter<bool> {
|
||||
public sealed class BooleanNormalizationConverter : JsonConverter<bool> {
|
||||
public override bool Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) =>
|
||||
reader.TokenType switch {
|
||||
JsonTokenType.True => true,
|
||||
JsonTokenType.False => false,
|
||||
JsonTokenType.Number => reader.GetByte() == 1,
|
||||
JsonTokenType.String => reader.GetString() == "1",
|
||||
_ => throw new JsonException()
|
||||
};
|
||||
|
||||
public override void Write(Utf8JsonWriter writer, bool value, JsonSerializerOptions options) {
|
||||
ArgumentNullException.ThrowIfNull(writer);
|
||||
|
||||
writer.WriteNumberValue(value ? 1 : 0);
|
||||
writer.WriteBooleanValue(value);
|
||||
}
|
||||
}
|
||||
@@ -28,6 +28,8 @@ using System.Linq;
|
||||
using System.Reflection;
|
||||
using System.Text.Encodings.Web;
|
||||
using System.Text.Json;
|
||||
using System.Text.Json.Nodes;
|
||||
using System.Text.Json.Serialization;
|
||||
using System.Text.Json.Serialization.Metadata;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
@@ -38,29 +40,33 @@ namespace ArchiSteamFarm.Helpers.Json;
|
||||
|
||||
public static class JsonUtilities {
|
||||
[PublicAPI]
|
||||
public static readonly JsonSerializerOptions DefaultJsonSerialierOptions = CreateDefaultJsonSerializerOptions();
|
||||
public static readonly JsonSerializerOptions DefaultJsonSerializerOptions = CreateDefaultJsonSerializerOptions();
|
||||
|
||||
[PublicAPI]
|
||||
public static readonly JsonSerializerOptions IndentedJsonSerialierOptions = CreateDefaultJsonSerializerOptions(true);
|
||||
public static readonly JsonSerializerOptions IndentedJsonSerializerOptions = CreateDefaultJsonSerializerOptions(true);
|
||||
|
||||
[PublicAPI]
|
||||
[UnconditionalSuppressMessage("AssemblyLoadTrimming", "IL2026:RequiresUnreferencedCode", Justification = "We don't care about trimmed assemblies, as we need it to work only with the known (used) ones")]
|
||||
public static JsonElement ToJsonElement<T>(this T obj) where T : notnull {
|
||||
ArgumentNullException.ThrowIfNull(obj);
|
||||
|
||||
return JsonSerializer.SerializeToElement(obj, DefaultJsonSerialierOptions);
|
||||
return JsonSerializer.SerializeToElement(obj, DefaultJsonSerializerOptions);
|
||||
}
|
||||
|
||||
[PublicAPI]
|
||||
[UnconditionalSuppressMessage("AssemblyLoadTrimming", "IL2026:RequiresUnreferencedCode", Justification = "We don't care about trimmed assemblies, as we need it to work only with the known (used) ones")]
|
||||
public static T? ToJsonObject<T>(this JsonElement jsonElement) => jsonElement.Deserialize<T>(DefaultJsonSerialierOptions);
|
||||
public static T? ToJsonObject<T>(this JsonElement jsonElement) => jsonElement.Deserialize<T>(DefaultJsonSerializerOptions);
|
||||
|
||||
[PublicAPI]
|
||||
[UnconditionalSuppressMessage("AssemblyLoadTrimming", "IL2026:RequiresUnreferencedCode", Justification = "We don't care about trimmed assemblies, as we need it to work only with the known (used) ones")]
|
||||
public static T? ToJsonObject<T>(this JsonNode jsonNode) => jsonNode.Deserialize<T>(DefaultJsonSerializerOptions);
|
||||
|
||||
[PublicAPI]
|
||||
[UnconditionalSuppressMessage("AssemblyLoadTrimming", "IL2026:RequiresUnreferencedCode", Justification = "We don't care about trimmed assemblies, as we need it to work only with the known (used) ones")]
|
||||
public static async ValueTask<T?> ToJsonObject<T>(this Stream stream, CancellationToken cancellationToken = default) {
|
||||
ArgumentNullException.ThrowIfNull(stream);
|
||||
|
||||
return await JsonSerializer.DeserializeAsync<T>(stream, DefaultJsonSerialierOptions, cancellationToken).ConfigureAwait(false);
|
||||
return await JsonSerializer.DeserializeAsync<T>(stream, DefaultJsonSerializerOptions, cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
[PublicAPI]
|
||||
@@ -68,12 +74,12 @@ public static class JsonUtilities {
|
||||
public static T? ToJsonObject<T>([StringSyntax(StringSyntaxAttribute.Json)] this string json) {
|
||||
ArgumentException.ThrowIfNullOrEmpty(json);
|
||||
|
||||
return JsonSerializer.Deserialize<T>(json, DefaultJsonSerialierOptions);
|
||||
return JsonSerializer.Deserialize<T>(json, DefaultJsonSerializerOptions);
|
||||
}
|
||||
|
||||
[PublicAPI]
|
||||
[UnconditionalSuppressMessage("AssemblyLoadTrimming", "IL2026:RequiresUnreferencedCode", Justification = "We don't care about trimmed assemblies, as we need it to work only with the known (used) ones")]
|
||||
public static string ToJsonText<T>(this T obj, bool writeIndented = false) => JsonSerializer.Serialize(obj, writeIndented ? IndentedJsonSerialierOptions : DefaultJsonSerialierOptions);
|
||||
public static string ToJsonText<T>(this T obj, bool writeIndented = false) => JsonSerializer.Serialize(obj, writeIndented ? IndentedJsonSerializerOptions : DefaultJsonSerializerOptions);
|
||||
|
||||
private static void ApplyCustomModifiers(JsonTypeInfo jsonTypeInfo) {
|
||||
ArgumentNullException.ThrowIfNull(jsonTypeInfo);
|
||||
@@ -111,12 +117,15 @@ public static class JsonUtilities {
|
||||
|
||||
[UnconditionalSuppressMessage("AssemblyLoadTrimming", "IL2026:RequiresUnreferencedCode", Justification = "We don't care about trimmed assemblies, as we need it to work only with the known (used) ones")]
|
||||
private static JsonSerializerOptions CreateDefaultJsonSerializerOptions(bool writeIndented = false) =>
|
||||
new() {
|
||||
new(JsonSerializerDefaults.Strict) {
|
||||
AllowTrailingCommas = true,
|
||||
Encoder = JavaScriptEncoder.UnsafeRelaxedJsonEscaping,
|
||||
IndentCharacter = '\t',
|
||||
IndentSize = 1,
|
||||
PropertyNamingPolicy = null,
|
||||
ReadCommentHandling = JsonCommentHandling.Skip,
|
||||
TypeInfoResolver = new DefaultJsonTypeInfoResolver { Modifiers = { ApplyCustomModifiers } },
|
||||
UnmappedMemberHandling = JsonUnmappedMemberHandling.Skip,
|
||||
WriteIndented = writeIndented
|
||||
};
|
||||
|
||||
|
||||
@@ -67,7 +67,7 @@ public abstract class SerializableFile : IDisposable {
|
||||
ArgumentNullException.ThrowIfNull(serializableFile);
|
||||
|
||||
if (string.IsNullOrEmpty(serializableFile.FilePath)) {
|
||||
throw new InvalidOperationException(nameof(serializableFile.FilePath));
|
||||
return;
|
||||
}
|
||||
|
||||
if (serializableFile.ReadOnly) {
|
||||
@@ -104,19 +104,11 @@ public abstract class SerializableFile : IDisposable {
|
||||
// We always want to write entire content to temporary file first, in order to never load corrupted data, also when target file doesn't exist
|
||||
string newFilePath = $"{serializableFile.FilePath}.new";
|
||||
|
||||
await File.WriteAllTextAsync(newFilePath, json).ConfigureAwait(false);
|
||||
|
||||
if (File.Exists(serializableFile.FilePath)) {
|
||||
string currentJson = await File.ReadAllTextAsync(serializableFile.FilePath).ConfigureAwait(false);
|
||||
|
||||
if (json == currentJson) {
|
||||
return;
|
||||
}
|
||||
|
||||
await File.WriteAllTextAsync(newFilePath, json).ConfigureAwait(false);
|
||||
|
||||
File.Replace(newFilePath, serializableFile.FilePath, null);
|
||||
} else {
|
||||
await File.WriteAllTextAsync(newFilePath, json).ConfigureAwait(false);
|
||||
|
||||
File.Move(newFilePath, serializableFile.FilePath);
|
||||
}
|
||||
} catch (Exception e) {
|
||||
@@ -155,19 +147,11 @@ public abstract class SerializableFile : IDisposable {
|
||||
try {
|
||||
// We always want to write entire content to temporary file first, in order to never load corrupted data, also when target file doesn't exist
|
||||
#pragma warning disable CA3003 // Ignored due to caller's intent
|
||||
await File.WriteAllTextAsync(newFilePath, json).ConfigureAwait(false);
|
||||
|
||||
if (File.Exists(filePath)) {
|
||||
string currentJson = await File.ReadAllTextAsync(filePath).ConfigureAwait(false);
|
||||
|
||||
if (json == currentJson) {
|
||||
return true;
|
||||
}
|
||||
|
||||
await File.WriteAllTextAsync(newFilePath, json).ConfigureAwait(false);
|
||||
|
||||
File.Replace(newFilePath, filePath, null);
|
||||
} else {
|
||||
await File.WriteAllTextAsync(newFilePath, json).ConfigureAwait(false);
|
||||
|
||||
File.Move(newFilePath, filePath);
|
||||
}
|
||||
#pragma warning restore CA3003 // Ignored due to caller's intent
|
||||
|
||||
@@ -30,13 +30,13 @@ using System.Net;
|
||||
using System.Reflection;
|
||||
using System.Text.Json;
|
||||
using System.Text.Json.Nodes;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using ArchiSteamFarm.Core;
|
||||
using ArchiSteamFarm.Helpers.Json;
|
||||
using ArchiSteamFarm.IPC.Controllers.Api;
|
||||
using ArchiSteamFarm.IPC.Integration;
|
||||
using ArchiSteamFarm.IPC.OpenApi;
|
||||
using ArchiSteamFarm.IPC.Swashbuckle;
|
||||
using ArchiSteamFarm.Localization;
|
||||
using ArchiSteamFarm.NLog;
|
||||
using ArchiSteamFarm.NLog.Targets;
|
||||
@@ -54,9 +54,9 @@ using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.FileProviders;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Net.Http.Headers;
|
||||
using Microsoft.OpenApi.Models;
|
||||
using NLog.Web;
|
||||
using IPNetwork = Microsoft.AspNetCore.HttpOverrides.IPNetwork;
|
||||
using Scalar.AspNetCore;
|
||||
using IPNetwork = System.Net.IPNetwork;
|
||||
|
||||
namespace ArchiSteamFarm.IPC;
|
||||
|
||||
@@ -65,6 +65,8 @@ internal static class ArchiKestrel {
|
||||
|
||||
internal static HistoryTarget? HistoryTarget { get; private set; }
|
||||
|
||||
private static readonly SemaphoreSlim StateSemaphore = new(1, 1);
|
||||
|
||||
private static WebApplication? WebApplication;
|
||||
|
||||
internal static void OnNewHistoryTarget(HistoryTarget? historyTarget = null) {
|
||||
@@ -79,49 +81,50 @@ internal static class ArchiKestrel {
|
||||
}
|
||||
}
|
||||
|
||||
internal static async Task Start() {
|
||||
if (WebApplication != null) {
|
||||
return;
|
||||
}
|
||||
|
||||
ASF.ArchiLogger.LogGenericInfo(Strings.IPCStarting);
|
||||
|
||||
// Init history logger for /Api/Log usage
|
||||
Logging.InitHistoryLogger();
|
||||
|
||||
WebApplication webApplication = await CreateWebApplication().ConfigureAwait(false);
|
||||
internal static async Task Restart() {
|
||||
await StateSemaphore.WaitAsync().ConfigureAwait(false);
|
||||
|
||||
try {
|
||||
// Start the server
|
||||
await webApplication.StartAsync().ConfigureAwait(false);
|
||||
} catch (Exception e) {
|
||||
ASF.ArchiLogger.LogGenericException(e);
|
||||
|
||||
await webApplication.DisposeAsync().ConfigureAwait(false);
|
||||
await StopInternally().ConfigureAwait(false);
|
||||
await StartInternally().ConfigureAwait(false);
|
||||
} finally {
|
||||
StateSemaphore.Release();
|
||||
}
|
||||
}
|
||||
|
||||
internal static async Task Start() {
|
||||
if (IsRunning) {
|
||||
return;
|
||||
}
|
||||
|
||||
WebApplication = webApplication;
|
||||
await StateSemaphore.WaitAsync().ConfigureAwait(false);
|
||||
|
||||
ASF.ArchiLogger.LogGenericInfo(Strings.IPCReady);
|
||||
try {
|
||||
await StartInternally().ConfigureAwait(false);
|
||||
} finally {
|
||||
StateSemaphore.Release();
|
||||
}
|
||||
}
|
||||
|
||||
internal static async Task Stop() {
|
||||
if (WebApplication == null) {
|
||||
if (!IsRunning) {
|
||||
return;
|
||||
}
|
||||
|
||||
await WebApplication.StopAsync().ConfigureAwait(false);
|
||||
await WebApplication.DisposeAsync().ConfigureAwait(false);
|
||||
await StateSemaphore.WaitAsync().ConfigureAwait(false);
|
||||
|
||||
WebApplication = null;
|
||||
try {
|
||||
await StopInternally().ConfigureAwait(false);
|
||||
} finally {
|
||||
StateSemaphore.Release();
|
||||
}
|
||||
}
|
||||
|
||||
[UnconditionalSuppressMessage("AssemblyLoadTrimming", "IL2026:RequiresUnreferencedCode", Justification = "PathString is a primitive, it's unlikely to be trimmed to the best of our knowledge")]
|
||||
[UnconditionalSuppressMessage("AssemblyLoadTrimming", "IL3000", Justification = "We don't care about trimmed assemblies, as we need it to work only with the known (used) ones")]
|
||||
private static void ConfigureApp([SuppressMessage("ReSharper", "SuggestBaseTypeForParameter")] ConfigurationManager configuration, WebApplication app) {
|
||||
private static void ConfigureApp([SuppressMessage("ReSharper", "SuggestBaseTypeForParameter")] ConfigurationManager configuration, IWebHostEnvironment environment, WebApplication app) {
|
||||
ArgumentNullException.ThrowIfNull(configuration);
|
||||
ArgumentNullException.ThrowIfNull(environment);
|
||||
ArgumentNullException.ThrowIfNull(app);
|
||||
|
||||
// The order of dependency injection is super important, doing things in wrong order will most likely break everything
|
||||
@@ -156,8 +159,10 @@ internal static class ArchiKestrel {
|
||||
// This must be called before default files, because we don't know the exact file name that will be used for index page
|
||||
app.UseWhen(static context => !context.Request.Path.StartsWithSegments("/Api", StringComparison.OrdinalIgnoreCase), static appBuilder => appBuilder.UseStatusCodePagesWithReExecute("/"));
|
||||
|
||||
// Add support for default root path redirection (GET / -> GET /index.html), must come before static files
|
||||
app.UseDefaultFiles();
|
||||
if (!string.IsNullOrEmpty(environment.WebRootPath)) {
|
||||
// Add support for default root path redirection (GET / -> GET /index.html), must come before static files
|
||||
app.UseDefaultFiles();
|
||||
}
|
||||
|
||||
// Add support for additional default files provided by plugins
|
||||
Dictionary<string, string> pluginPaths = new(StringComparer.Ordinal);
|
||||
@@ -220,12 +225,14 @@ internal static class ArchiKestrel {
|
||||
app.UseStaticFiles(options);
|
||||
}
|
||||
|
||||
// Add support for static files (e.g. HTML, CSS and JS from IPC GUI)
|
||||
app.UseStaticFiles(
|
||||
new StaticFileOptions {
|
||||
OnPrepareResponse = OnPrepareResponse
|
||||
}
|
||||
);
|
||||
if (!string.IsNullOrEmpty(environment.WebRootPath)) {
|
||||
// Add support for static files (e.g. HTML, CSS and JS from IPC GUI)
|
||||
app.UseStaticFiles(
|
||||
new StaticFileOptions {
|
||||
OnPrepareResponse = OnPrepareResponse
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
// Use routing for our API controllers, this should be called once we're done with all the static files mess
|
||||
app.UseRouting();
|
||||
@@ -244,6 +251,11 @@ internal static class ArchiKestrel {
|
||||
// Add support for websockets that we use e.g. in /Api/NLog
|
||||
app.UseWebSockets();
|
||||
|
||||
// Add support for output caching
|
||||
if (ASF.GlobalConfig?.OptimizationMode != GlobalConfig.EOptimizationMode.MinMemoryUsage) {
|
||||
app.UseOutputCache();
|
||||
}
|
||||
|
||||
// Add additional endpoints provided by plugins
|
||||
foreach (IWebServiceProvider plugin in PluginsCore.ActivePlugins.OfType<IWebServiceProvider>()) {
|
||||
try {
|
||||
@@ -257,21 +269,19 @@ internal static class ArchiKestrel {
|
||||
app.MapControllers();
|
||||
|
||||
// Add support for OpenAPI, responsible for automatic API documentation generation, this should be on the end, once we're done with API
|
||||
if (Program.UseOpenApi) {
|
||||
app.MapOpenApi("/swagger/{documentName}/swagger.json");
|
||||
} else {
|
||||
app.UseSwagger();
|
||||
IEndpointConventionBuilder openApi = app.MapOpenApi("/swagger/{documentName}/swagger.json");
|
||||
|
||||
if (ASF.GlobalConfig?.OptimizationMode != GlobalConfig.EOptimizationMode.MinMemoryUsage) {
|
||||
openApi.CacheOutput();
|
||||
}
|
||||
|
||||
// Add support for swagger UI, this should be after swagger, obviously
|
||||
app.UseSwaggerUI(
|
||||
static options => {
|
||||
options.DisplayRequestDuration();
|
||||
options.EnableDeepLinking();
|
||||
options.EnableTryItOutByDefault();
|
||||
options.ShowCommonExtensions();
|
||||
options.ShowExtensions();
|
||||
options.SwaggerEndpoint($"{SharedInfo.ASF}/swagger.json", $"{SharedInfo.ASF} API");
|
||||
app.MapScalarApiReference(
|
||||
"/swagger", static options => {
|
||||
options.DefaultFonts = false;
|
||||
options.OpenApiRoutePattern = $"/swagger/{SharedInfo.ASF}/swagger.json";
|
||||
options.Theme = ScalarTheme.Kepler;
|
||||
options.Title = $"{SharedInfo.AssemblyName} API";
|
||||
}
|
||||
);
|
||||
}
|
||||
@@ -308,13 +318,12 @@ internal static class ArchiKestrel {
|
||||
}
|
||||
|
||||
// Add support for proxies
|
||||
services.Configure<ForwardedHeadersOptions>(
|
||||
options => {
|
||||
services.Configure<ForwardedHeadersOptions>(options => {
|
||||
options.ForwardedHeaders = ForwardedHeaders.All;
|
||||
|
||||
if (knownNetworks != null) {
|
||||
foreach (IPNetwork knownNetwork in knownNetworks) {
|
||||
options.KnownNetworks.Add(knownNetwork);
|
||||
options.KnownIPNetworks.Add(knownNetwork);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -337,74 +346,21 @@ internal static class ArchiKestrel {
|
||||
services.AddCors(static options => options.AddDefaultPolicy(static policyBuilder => policyBuilder.AllowAnyOrigin()));
|
||||
}
|
||||
|
||||
// Add support for OpenAPI, responsible for automatic API documentation generation
|
||||
if (Program.UseOpenApi) {
|
||||
services.AddOpenApi(
|
||||
SharedInfo.ASF, static options => {
|
||||
options.AddDocumentTransformer<DocumentTransformer>();
|
||||
options.AddOperationTransformer<OperationTransformer>();
|
||||
options.AddSchemaTransformer<SchemaTransformer>();
|
||||
}
|
||||
);
|
||||
} else {
|
||||
services.AddSwaggerGen(
|
||||
static options => {
|
||||
options.AddSecurityDefinition(
|
||||
nameof(GlobalConfig.IPCPassword), new OpenApiSecurityScheme {
|
||||
Description = $"{nameof(GlobalConfig.IPCPassword)} authentication using request headers. Check {SharedInfo.ProjectURL}/wiki/IPC#authentication for more info.",
|
||||
In = ParameterLocation.Header,
|
||||
Name = ApiAuthenticationMiddleware.HeadersField,
|
||||
Type = SecuritySchemeType.ApiKey
|
||||
}
|
||||
);
|
||||
|
||||
options.AddSecurityRequirement(
|
||||
new OpenApiSecurityRequirement {
|
||||
{
|
||||
new OpenApiSecurityScheme {
|
||||
Reference = new OpenApiReference {
|
||||
Id = nameof(GlobalConfig.IPCPassword),
|
||||
Type = ReferenceType.SecurityScheme
|
||||
}
|
||||
},
|
||||
|
||||
[]
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
// We require custom schema IDs due to conflicting type names, choosing the proper one is tricky as there is no good answer and any kind of convention has a potential to create conflict
|
||||
// FullName and Name both do, ToString() for unknown to me reason doesn't, and I don't have courage to call our WebUtilities.GetUnifiedName() better than what .NET ships with (because it isn't)
|
||||
// Let's use ToString() until we find a good enough reason to change it, also, the name must pass ^[a-zA-Z0-9.-_]+$ regex
|
||||
options.CustomSchemaIds(static type => type.ToString().Replace('+', '-'));
|
||||
|
||||
options.EnableAnnotations(true, true);
|
||||
|
||||
options.SchemaFilter<CustomAttributesSchemaFilter>();
|
||||
options.SchemaFilter<EnumSchemaFilter>();
|
||||
options.SchemaFilter<ReadOnlyFixesSchemaFilter>();
|
||||
|
||||
options.SwaggerDoc(
|
||||
SharedInfo.ASF, new OpenApiInfo {
|
||||
Contact = new OpenApiContact {
|
||||
Name = SharedInfo.GithubRepo,
|
||||
Url = new Uri(SharedInfo.ProjectURL)
|
||||
},
|
||||
|
||||
License = new OpenApiLicense {
|
||||
Name = SharedInfo.LicenseName,
|
||||
Url = new Uri(SharedInfo.LicenseURL)
|
||||
},
|
||||
|
||||
Title = $"{SharedInfo.AssemblyName} API",
|
||||
Version = SharedInfo.Version.ToString()
|
||||
}
|
||||
);
|
||||
}
|
||||
);
|
||||
// Add support for output caching
|
||||
if (ASF.GlobalConfig?.OptimizationMode != GlobalConfig.EOptimizationMode.MinMemoryUsage) {
|
||||
services.AddOutputCache();
|
||||
}
|
||||
|
||||
// Add support for optional healtchecks
|
||||
// Add support for OpenAPI, responsible for automatic API documentation generation
|
||||
services.AddOpenApi(
|
||||
SharedInfo.ASF, static options => {
|
||||
options.AddDocumentTransformer<DocumentTransformer>();
|
||||
options.AddOperationTransformer<OperationTransformer>();
|
||||
options.AddSchemaTransformer<SchemaTransformer>();
|
||||
}
|
||||
);
|
||||
|
||||
// Add support for optional health-checks
|
||||
services.AddHealthChecks();
|
||||
|
||||
// Add support for additional services provided by plugins
|
||||
@@ -416,9 +372,8 @@ internal static class ArchiKestrel {
|
||||
}
|
||||
}
|
||||
|
||||
services.ConfigureHttpJsonOptions(
|
||||
static options => {
|
||||
JsonSerializerOptions jsonSerializerOptions = Debugging.IsUserDebugging ? JsonUtilities.IndentedJsonSerialierOptions : JsonUtilities.DefaultJsonSerialierOptions;
|
||||
services.ConfigureHttpJsonOptions(static options => {
|
||||
JsonSerializerOptions jsonSerializerOptions = Debugging.IsUserDebugging ? JsonUtilities.IndentedJsonSerializerOptions : JsonUtilities.DefaultJsonSerializerOptions;
|
||||
|
||||
options.SerializerOptions.PropertyNamingPolicy = jsonSerializerOptions.PropertyNamingPolicy;
|
||||
options.SerializerOptions.TypeInfoResolver = jsonSerializerOptions.TypeInfoResolver;
|
||||
@@ -442,9 +397,8 @@ internal static class ArchiKestrel {
|
||||
mvc.AddControllersAsServices();
|
||||
|
||||
// Modify default JSON options
|
||||
mvc.AddJsonOptions(
|
||||
static options => {
|
||||
JsonSerializerOptions jsonSerializerOptions = Debugging.IsUserDebugging ? JsonUtilities.IndentedJsonSerialierOptions : JsonUtilities.DefaultJsonSerialierOptions;
|
||||
mvc.AddJsonOptions(static options => {
|
||||
JsonSerializerOptions jsonSerializerOptions = Debugging.IsUserDebugging ? JsonUtilities.IndentedJsonSerializerOptions : JsonUtilities.DefaultJsonSerializerOptions;
|
||||
|
||||
options.JsonSerializerOptions.PropertyNamingPolicy = jsonSerializerOptions.PropertyNamingPolicy;
|
||||
options.JsonSerializerOptions.TypeInfoResolver = jsonSerializerOptions.TypeInfoResolver;
|
||||
@@ -504,8 +458,7 @@ internal static class ArchiKestrel {
|
||||
builder.WebHost.UseConfiguration(new ConfigurationBuilder().SetBasePath(absoluteConfigDirectory).AddJsonFile(SharedInfo.IPCConfigFile, false, true).Build());
|
||||
}
|
||||
|
||||
builder.WebHost.ConfigureKestrel(
|
||||
options => {
|
||||
builder.WebHost.ConfigureKestrel(options => {
|
||||
options.AddServerHeader = false;
|
||||
|
||||
if (customConfigExists) {
|
||||
@@ -530,7 +483,7 @@ internal static class ArchiKestrel {
|
||||
|
||||
WebApplication result = builder.Build();
|
||||
|
||||
ConfigureApp(builder.Configuration, result);
|
||||
ConfigureApp(builder.Configuration, builder.Environment, result);
|
||||
|
||||
return result;
|
||||
}
|
||||
@@ -569,4 +522,43 @@ internal static class ArchiKestrel {
|
||||
|
||||
headers.CacheControl = cacheControl;
|
||||
}
|
||||
|
||||
private static async Task StartInternally() {
|
||||
if (WebApplication != null) {
|
||||
return;
|
||||
}
|
||||
|
||||
ASF.ArchiLogger.LogGenericInfo(Strings.IPCStarting);
|
||||
|
||||
// Init history logger for /Api/Log usage
|
||||
Logging.InitHistoryLogger();
|
||||
|
||||
WebApplication webApplication = await CreateWebApplication().ConfigureAwait(false);
|
||||
|
||||
try {
|
||||
// Start the server
|
||||
await webApplication.StartAsync().ConfigureAwait(false);
|
||||
} catch (Exception e) {
|
||||
ASF.ArchiLogger.LogGenericException(e);
|
||||
|
||||
await webApplication.DisposeAsync().ConfigureAwait(false);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
WebApplication = webApplication;
|
||||
|
||||
ASF.ArchiLogger.LogGenericInfo(Strings.IPCReady);
|
||||
}
|
||||
|
||||
private static async Task StopInternally() {
|
||||
if (WebApplication == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
await WebApplication.StopAsync().ConfigureAwait(false);
|
||||
await WebApplication.DisposeAsync().ConfigureAwait(false);
|
||||
|
||||
WebApplication = null;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -23,7 +23,7 @@
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.Specialized;
|
||||
using System.Collections.Immutable;
|
||||
using System.Linq;
|
||||
using System.Net;
|
||||
using System.Text.Json;
|
||||
@@ -33,6 +33,7 @@ using ArchiSteamFarm.IPC.Requests;
|
||||
using ArchiSteamFarm.IPC.Responses;
|
||||
using ArchiSteamFarm.Localization;
|
||||
using ArchiSteamFarm.Steam;
|
||||
using ArchiSteamFarm.Steam.Data;
|
||||
using ArchiSteamFarm.Steam.Storage;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
@@ -151,6 +152,10 @@ public sealed class BotController : ArchiController {
|
||||
request.BotConfig.SteamParentalCode = bot.BotConfig.SteamParentalCode;
|
||||
}
|
||||
|
||||
if (!request.BotConfig.IsWebProxyPasswordSet && bot.BotConfig.IsWebProxyPasswordSet) {
|
||||
request.BotConfig.WebProxyPassword = bot.BotConfig.WebProxyPassword;
|
||||
}
|
||||
|
||||
if (bot.BotConfig.AdditionalProperties?.Count > 0) {
|
||||
request.BotConfig.AdditionalProperties ??= new Dictionary<string, JsonElement>(bot.BotConfig.AdditionalProperties.Count, bot.BotConfig.AdditionalProperties.Comparer);
|
||||
|
||||
@@ -221,7 +226,7 @@ public sealed class BotController : ArchiController {
|
||||
|
||||
[EndpointSummary("Adds keys to redeem using BGR to given bot")]
|
||||
[HttpPost("{botNames:required}/GamesToRedeemInBackground")]
|
||||
[ProducesResponseType<GenericResponse<IReadOnlyDictionary<string, IOrderedDictionary>>>((int) HttpStatusCode.OK)]
|
||||
[ProducesResponseType<GenericResponse<IReadOnlyDictionary<string, OrderedDictionary<string, string>>>>((int) HttpStatusCode.OK)]
|
||||
[ProducesResponseType<GenericResponse>((int) HttpStatusCode.BadRequest)]
|
||||
public async Task<ActionResult<GenericResponse>> GamesToRedeemInBackgroundPost(string botNames, [FromBody] BotGamesToRedeemInBackgroundRequest request) {
|
||||
ArgumentException.ThrowIfNullOrEmpty(botNames);
|
||||
@@ -237,21 +242,21 @@ public sealed class BotController : ArchiController {
|
||||
return BadRequest(new GenericResponse(false, Strings.FormatBotNotFound(botNames)));
|
||||
}
|
||||
|
||||
IOrderedDictionary validGamesToRedeemInBackground = Bot.ValidateGamesToRedeemInBackground(request.GamesToRedeemInBackground);
|
||||
Bot.FilterGamesToRedeemInBackground(request.GamesToRedeemInBackground);
|
||||
|
||||
if (validGamesToRedeemInBackground.Count == 0) {
|
||||
return BadRequest(new GenericResponse(false, Strings.FormatErrorIsEmpty(nameof(validGamesToRedeemInBackground))));
|
||||
if (request.GamesToRedeemInBackground.Count == 0) {
|
||||
return BadRequest(new GenericResponse(false, Strings.FormatErrorIsEmpty(nameof(request.GamesToRedeemInBackground))));
|
||||
}
|
||||
|
||||
await Utilities.InParallel(bots.Select(bot => Task.Run(() => bot.AddGamesToRedeemInBackground(validGamesToRedeemInBackground)))).ConfigureAwait(false);
|
||||
await Utilities.InParallel(bots.Select(bot => Task.Run(() => bot.AddGamesToRedeemInBackground(request.GamesToRedeemInBackground)))).ConfigureAwait(false);
|
||||
|
||||
Dictionary<string, IOrderedDictionary> result = new(bots.Count, Bot.BotsComparer);
|
||||
Dictionary<string, OrderedDictionary<string, string>> result = new(bots.Count, Bot.BotsComparer);
|
||||
|
||||
foreach (Bot bot in bots) {
|
||||
result[bot.BotName] = validGamesToRedeemInBackground;
|
||||
result[bot.BotName] = request.GamesToRedeemInBackground;
|
||||
}
|
||||
|
||||
return Ok(new GenericResponse<IReadOnlyDictionary<string, IOrderedDictionary>>(result));
|
||||
return Ok(new GenericResponse<IReadOnlyDictionary<string, OrderedDictionary<string, string>>>(result));
|
||||
}
|
||||
|
||||
[EndpointSummary("Provides input value to given bot for next usage")]
|
||||
@@ -277,6 +282,81 @@ public sealed class BotController : ArchiController {
|
||||
return Ok(results.All(static result => result) ? new GenericResponse(true) : new GenericResponse(false, Strings.WarningFailed));
|
||||
}
|
||||
|
||||
[EndpointSummary("Fetches specific inventory of given bots")]
|
||||
[HttpGet("{botNames:required}/Inventory/{appID}/{contextID}")]
|
||||
[ProducesResponseType<GenericResponse<IReadOnlyDictionary<string, BotInventoryResponse>>>((int) HttpStatusCode.OK)]
|
||||
[ProducesResponseType<GenericResponse>((int) HttpStatusCode.BadRequest)]
|
||||
public async Task<ActionResult<GenericResponse>> InventoryGet(string botNames, uint appID, ulong contextID, [FromQuery] string? language = null) {
|
||||
ArgumentException.ThrowIfNullOrEmpty(botNames);
|
||||
|
||||
if (appID == 0) {
|
||||
return BadRequest(new GenericResponse(false, Strings.FormatErrorIsInvalid(nameof(appID))));
|
||||
}
|
||||
|
||||
if (contextID == 0) {
|
||||
return BadRequest(new GenericResponse(false, Strings.FormatErrorIsInvalid(nameof(contextID))));
|
||||
}
|
||||
|
||||
HashSet<Bot>? bots = Bot.GetBots(botNames);
|
||||
|
||||
if ((bots == null) || (bots.Count == 0)) {
|
||||
return BadRequest(new GenericResponse(false, Strings.FormatBotNotFound(botNames)));
|
||||
}
|
||||
|
||||
IList<(HashSet<Asset>? Result, string Message)> results = await Utilities.InParallel(bots.Select(bot => bot.Actions.GetInventory(appID, contextID, language: language))).ConfigureAwait(false);
|
||||
|
||||
Dictionary<string, BotInventoryResponse> result = new(bots.Count, Bot.BotsComparer);
|
||||
|
||||
foreach (Bot bot in bots) {
|
||||
(HashSet<Asset>? inventory, _) = results[result.Count];
|
||||
|
||||
if (inventory == null) {
|
||||
result[bot.BotName] = new BotInventoryResponse();
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
HashSet<CEcon_Asset> assets = new(inventory.Count);
|
||||
HashSet<CEconItem_Description> descriptions = [];
|
||||
|
||||
foreach (Asset asset in inventory) {
|
||||
assets.Add(asset.Body);
|
||||
|
||||
if (asset.Description != null) {
|
||||
descriptions.Add(asset.Description.Body);
|
||||
}
|
||||
}
|
||||
|
||||
result[bot.BotName] = new BotInventoryResponse(assets, descriptions);
|
||||
}
|
||||
|
||||
return Ok(new GenericResponse<IReadOnlyDictionary<string, BotInventoryResponse>>(result));
|
||||
}
|
||||
|
||||
[EndpointSummary("Fetches general inventory information of given bots")]
|
||||
[HttpGet("{botNames:required}/Inventory")]
|
||||
[ProducesResponseType<GenericResponse<IReadOnlyDictionary<string, ImmutableDictionary<uint, InventoryAppData>>>>((int) HttpStatusCode.OK)]
|
||||
[ProducesResponseType<GenericResponse>((int) HttpStatusCode.BadRequest)]
|
||||
public async Task<ActionResult<GenericResponse>> InventoryInfoGet(string botNames) {
|
||||
ArgumentException.ThrowIfNullOrEmpty(botNames);
|
||||
|
||||
HashSet<Bot>? bots = Bot.GetBots(botNames);
|
||||
|
||||
if ((bots == null) || (bots.Count == 0)) {
|
||||
return BadRequest(new GenericResponse(false, Strings.FormatBotNotFound(botNames)));
|
||||
}
|
||||
|
||||
IList<ImmutableDictionary<uint, InventoryAppData>?> results = await Utilities.InParallel(bots.Select(static bot => bot.ArchiWebHandler.GetInventoryContextData())).ConfigureAwait(false);
|
||||
|
||||
Dictionary<string, ImmutableDictionary<uint, InventoryAppData>?> result = new(bots.Count, Bot.BotsComparer);
|
||||
|
||||
foreach (Bot bot in bots) {
|
||||
result[bot.BotName] = results[result.Count];
|
||||
}
|
||||
|
||||
return Ok(new GenericResponse<IReadOnlyDictionary<string, ImmutableDictionary<uint, InventoryAppData>?>>(result));
|
||||
}
|
||||
|
||||
[EndpointSummary("Pauses given bots")]
|
||||
[HttpPost("{botNames:required}/Pause")]
|
||||
[ProducesResponseType<GenericResponse>((int) HttpStatusCode.OK)]
|
||||
@@ -358,6 +438,35 @@ public sealed class BotController : ArchiController {
|
||||
return Ok(new GenericResponse<IReadOnlyDictionary<string, IReadOnlyDictionary<string, CStore_RegisterCDKey_Response?>>>(result.Values.SelectMany(static responses => responses.Values).All(static value => value != null), result));
|
||||
}
|
||||
|
||||
[EndpointSummary("Removes licenses on given bots")]
|
||||
[HttpPost("{botNames:required}/RemoveLicense")]
|
||||
[ProducesResponseType<GenericResponse<IReadOnlyDictionary<string, BotRemoveLicenseResponse>>>((int) HttpStatusCode.OK)]
|
||||
[ProducesResponseType<GenericResponse>((int) HttpStatusCode.BadRequest)]
|
||||
public async Task<ActionResult<GenericResponse>> RemoveLicensePost(string botNames, [FromBody] BotRemoveLicenseRequest request) {
|
||||
ArgumentException.ThrowIfNullOrEmpty(botNames);
|
||||
ArgumentNullException.ThrowIfNull(request);
|
||||
|
||||
if ((request.Apps?.IsEmpty != false) && (request.Packages?.IsEmpty != false)) {
|
||||
return BadRequest(new GenericResponse(false, Strings.FormatErrorIsEmpty($"{nameof(request.Apps)} && {nameof(request.Packages)}")));
|
||||
}
|
||||
|
||||
HashSet<Bot>? bots = Bot.GetBots(botNames);
|
||||
|
||||
if ((bots == null) || (bots.Count == 0)) {
|
||||
return BadRequest(new GenericResponse(false, Strings.FormatBotNotFound(botNames)));
|
||||
}
|
||||
|
||||
IList<BotRemoveLicenseResponse> results = await Utilities.InParallel(bots.Select(bot => RemoveLicense(bot, request))).ConfigureAwait(false);
|
||||
|
||||
Dictionary<string, BotRemoveLicenseResponse> result = new(bots.Count, Bot.BotsComparer);
|
||||
|
||||
foreach (Bot bot in bots) {
|
||||
result[bot.BotName] = results[result.Count];
|
||||
}
|
||||
|
||||
return Ok(new GenericResponse<IReadOnlyDictionary<string, BotRemoveLicenseResponse>>(result));
|
||||
}
|
||||
|
||||
[EndpointSummary("Renames given bot along with all its related files")]
|
||||
[HttpPost("{botName:required}/Rename")]
|
||||
[ProducesResponseType<GenericResponse>((int) HttpStatusCode.OK)]
|
||||
@@ -478,4 +587,42 @@ public sealed class BotController : ArchiController {
|
||||
|
||||
return new BotAddLicenseResponse(apps, packages);
|
||||
}
|
||||
|
||||
private static async Task<BotRemoveLicenseResponse> RemoveLicense(Bot bot, BotRemoveLicenseRequest request) {
|
||||
ArgumentNullException.ThrowIfNull(bot);
|
||||
ArgumentNullException.ThrowIfNull(request);
|
||||
|
||||
Dictionary<uint, EResult>? apps = null;
|
||||
Dictionary<uint, EResult>? packages = null;
|
||||
|
||||
if (request.Apps != null) {
|
||||
apps = new Dictionary<uint, EResult>(request.Apps.Count);
|
||||
|
||||
foreach (uint appID in request.Apps) {
|
||||
if (!bot.IsConnectedAndLoggedOn) {
|
||||
apps[appID] = EResult.Timeout;
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
apps[appID] = await bot.Actions.RemoveLicenseApp(appID).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
|
||||
if (request.Packages != null) {
|
||||
packages = new Dictionary<uint, EResult>(request.Packages.Count);
|
||||
|
||||
foreach (uint subID in request.Packages) {
|
||||
if (!bot.IsConnectedAndLoggedOn) {
|
||||
packages[subID] = EResult.Timeout;
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
packages[subID] = await bot.Actions.RemoveLicensePackage(subID).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
|
||||
return new BotRemoveLicenseResponse(apps, packages);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -70,10 +70,6 @@ public sealed class NLogController : ArchiController {
|
||||
return BadRequest(new GenericResponse(false, Strings.FormatErrorIsInvalid(nameof(lastAt))));
|
||||
}
|
||||
|
||||
if (!Logging.LogFileExists) {
|
||||
return BadRequest(new GenericResponse(false, Strings.FormatErrorIsEmpty(nameof(SharedInfo.LogFile))));
|
||||
}
|
||||
|
||||
string[]? lines = await Logging.ReadLogFileLines().ConfigureAwait(false);
|
||||
|
||||
if ((lines == null) || (lines.Length == 0)) {
|
||||
|
||||
@@ -124,19 +124,19 @@ internal sealed class ApiAuthenticationMiddleware {
|
||||
return (HttpStatusCode.OK, true);
|
||||
}
|
||||
|
||||
if (ForwardedHeadersOptions.KnownNetworks.Count == 0) {
|
||||
if (ForwardedHeadersOptions.KnownIPNetworks.Count == 0) {
|
||||
return (HttpStatusCode.Forbidden, true);
|
||||
}
|
||||
|
||||
if (clientIP.IsIPv4MappedToIPv6) {
|
||||
IPAddress mappedClientIP = clientIP.MapToIPv4();
|
||||
|
||||
if (ForwardedHeadersOptions.KnownNetworks.Any(network => network.Contains(mappedClientIP))) {
|
||||
if (ForwardedHeadersOptions.KnownIPNetworks.Any(network => network.Contains(mappedClientIP))) {
|
||||
return (HttpStatusCode.OK, true);
|
||||
}
|
||||
}
|
||||
|
||||
return (ForwardedHeadersOptions.KnownNetworks.Any(network => network.Contains(clientIP)) ? HttpStatusCode.OK : HttpStatusCode.Forbidden, true);
|
||||
return (ForwardedHeadersOptions.KnownIPNetworks.Any(network => network.Contains(clientIP)) ? HttpStatusCode.OK : HttpStatusCode.Forbidden, true);
|
||||
}
|
||||
|
||||
if (!context.Request.Headers.TryGetValue(HeadersField, out StringValues passwords) && !context.Request.Query.TryGetValue("password", out passwords)) {
|
||||
|
||||
@@ -23,7 +23,7 @@
|
||||
|
||||
using System;
|
||||
using JetBrains.Annotations;
|
||||
using Microsoft.OpenApi.Models;
|
||||
using Microsoft.OpenApi;
|
||||
|
||||
namespace ArchiSteamFarm.IPC.Integration;
|
||||
|
||||
|
||||
@@ -22,8 +22,9 @@
|
||||
// limitations under the License.
|
||||
|
||||
using System;
|
||||
using System.Globalization;
|
||||
using JetBrains.Annotations;
|
||||
using Microsoft.OpenApi.Models;
|
||||
using Microsoft.OpenApi;
|
||||
|
||||
namespace ArchiSteamFarm.IPC.Integration;
|
||||
|
||||
@@ -45,16 +46,16 @@ public sealed class SwaggerItemsMinMaxAttribute : CustomSwaggerAttribute {
|
||||
public override void Apply(OpenApiSchema schema) {
|
||||
ArgumentNullException.ThrowIfNull(schema);
|
||||
|
||||
if (schema.Items == null) {
|
||||
if (schema.Items is not OpenApiSchema items) {
|
||||
throw new InvalidOperationException(nameof(schema.Items));
|
||||
}
|
||||
|
||||
if (BackingMinimum.HasValue) {
|
||||
schema.Items.Minimum = BackingMinimum.Value;
|
||||
items.Minimum = BackingMinimum.Value.ToString(CultureInfo.InvariantCulture);
|
||||
}
|
||||
|
||||
if (BackingMaximum.HasValue) {
|
||||
schema.Items.Maximum = BackingMaximum.Value;
|
||||
items.Maximum = BackingMaximum.Value.ToString(CultureInfo.InvariantCulture);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -22,10 +22,9 @@
|
||||
// limitations under the License.
|
||||
|
||||
using System;
|
||||
using System.Text.Json.Nodes;
|
||||
using JetBrains.Annotations;
|
||||
using Microsoft.OpenApi.Any;
|
||||
using Microsoft.OpenApi.Extensions;
|
||||
using Microsoft.OpenApi.Models;
|
||||
using Microsoft.OpenApi;
|
||||
|
||||
namespace ArchiSteamFarm.IPC.Integration;
|
||||
|
||||
@@ -36,10 +35,20 @@ public sealed class SwaggerSecurityCriticalAttribute : CustomSwaggerAttribute {
|
||||
public override void Apply(OpenApiSchema schema) {
|
||||
ArgumentNullException.ThrowIfNull(schema);
|
||||
|
||||
if (schema.Items is { Reference: null }) {
|
||||
schema.Items.AddExtension(ExtensionName, new OpenApiBoolean(true));
|
||||
} else {
|
||||
schema.AddExtension(ExtensionName, new OpenApiBoolean(true));
|
||||
JsonValue value = JsonValue.Create(true);
|
||||
|
||||
if (schema.Items != null) {
|
||||
if (schema.Items is OpenApiSchema items) {
|
||||
items.AddExtension(ExtensionName, new JsonNodeExtension(value));
|
||||
} else if (schema.Items.Extensions != null) {
|
||||
schema.Items.Extensions[ExtensionName] = new JsonNodeExtension(value);
|
||||
} else {
|
||||
throw new InvalidOperationException(nameof(schema.Items));
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
schema.AddExtension(ExtensionName, new JsonNodeExtension(value));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -22,8 +22,9 @@
|
||||
// limitations under the License.
|
||||
|
||||
using System;
|
||||
using System.Globalization;
|
||||
using JetBrains.Annotations;
|
||||
using Microsoft.OpenApi.Models;
|
||||
using Microsoft.OpenApi;
|
||||
using SteamKit2;
|
||||
|
||||
namespace ArchiSteamFarm.IPC.Integration;
|
||||
@@ -38,7 +39,7 @@ public sealed class SwaggerSteamIdentifierAttribute : CustomSwaggerAttribute {
|
||||
public override void Apply(OpenApiSchema schema) {
|
||||
ArgumentNullException.ThrowIfNull(schema);
|
||||
|
||||
schema.Minimum = new SteamID(MinimumAccountID, Universe, AccountType);
|
||||
schema.Maximum = new SteamID(MaximumAccountID, Universe, AccountType);
|
||||
schema.Minimum = new SteamID(MinimumAccountID, Universe, AccountType).ConvertToUInt64().ToString(CultureInfo.InvariantCulture);
|
||||
schema.Maximum = new SteamID(MaximumAccountID, Universe, AccountType).ConvertToUInt64().ToString(CultureInfo.InvariantCulture);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -22,36 +22,50 @@
|
||||
// limitations under the License.
|
||||
|
||||
using System;
|
||||
using System.Linq;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.Text.Json.Nodes;
|
||||
using JetBrains.Annotations;
|
||||
using Microsoft.OpenApi.Any;
|
||||
using Microsoft.OpenApi.Extensions;
|
||||
using Microsoft.OpenApi.Models;
|
||||
using Microsoft.OpenApi;
|
||||
|
||||
namespace ArchiSteamFarm.IPC.Integration;
|
||||
|
||||
[PublicAPI]
|
||||
public sealed class SwaggerValidValuesAttribute : CustomSwaggerAttribute {
|
||||
private const string ExtensionName = "x-valid-values";
|
||||
|
||||
public int[]? ValidIntValues { get; init; }
|
||||
public string[]? ValidStringValues { get; init; }
|
||||
|
||||
[UnconditionalSuppressMessage("AssemblyLoadTrimming", "IL2026:RequiresUnreferencedCode", Justification = "We're not creating json values with non-primitive types")]
|
||||
public override void Apply(OpenApiSchema schema) {
|
||||
ArgumentNullException.ThrowIfNull(schema);
|
||||
|
||||
OpenApiArray validValues = [];
|
||||
JsonArray validValues = [];
|
||||
|
||||
if (ValidIntValues != null) {
|
||||
validValues.AddRange(ValidIntValues.Select(static type => new OpenApiInteger(type)));
|
||||
foreach (int value in ValidIntValues) {
|
||||
validValues.Add(JsonValue.Create(value));
|
||||
}
|
||||
}
|
||||
|
||||
if (ValidStringValues != null) {
|
||||
validValues.AddRange(ValidStringValues.Select(static type => new OpenApiString(type)));
|
||||
foreach (string value in ValidStringValues) {
|
||||
validValues.Add(JsonValue.Create(value));
|
||||
}
|
||||
}
|
||||
|
||||
if (schema.Items is { Reference: null }) {
|
||||
schema.Items.AddExtension("x-valid-values", validValues);
|
||||
} else {
|
||||
schema.AddExtension("x-valid-values", validValues);
|
||||
if (schema.Items != null) {
|
||||
if (schema.Items is OpenApiSchema items) {
|
||||
items.AddExtension(ExtensionName, new JsonNodeExtension(validValues));
|
||||
} else if (schema.Items.Extensions != null) {
|
||||
schema.Items.Extensions[ExtensionName] = new JsonNodeExtension(validValues);
|
||||
} else {
|
||||
throw new InvalidOperationException(nameof(schema.Items));
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
schema.AddExtension(ExtensionName, new JsonNodeExtension(validValues));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -29,7 +29,7 @@ using ArchiSteamFarm.IPC.Integration;
|
||||
using ArchiSteamFarm.Storage;
|
||||
using JetBrains.Annotations;
|
||||
using Microsoft.AspNetCore.OpenApi;
|
||||
using Microsoft.OpenApi.Models;
|
||||
using Microsoft.OpenApi;
|
||||
|
||||
namespace ArchiSteamFarm.IPC.OpenApi;
|
||||
|
||||
@@ -40,7 +40,6 @@ internal sealed class DocumentTransformer : IOpenApiDocumentTransformer {
|
||||
ArgumentNullException.ThrowIfNull(document);
|
||||
ArgumentNullException.ThrowIfNull(context);
|
||||
|
||||
document.Info ??= new OpenApiInfo();
|
||||
document.Info.Title = $"{SharedInfo.AssemblyName} API";
|
||||
document.Info.Version = SharedInfo.Version.ToString();
|
||||
|
||||
@@ -53,7 +52,7 @@ internal sealed class DocumentTransformer : IOpenApiDocumentTransformer {
|
||||
document.Info.License.Url = new Uri(SharedInfo.LicenseURL);
|
||||
|
||||
document.Components ??= new OpenApiComponents();
|
||||
document.Components.SecuritySchemes ??= new Dictionary<string, OpenApiSecurityScheme>(1);
|
||||
document.Components.SecuritySchemes ??= new Dictionary<string, IOpenApiSecurityScheme>(1);
|
||||
|
||||
document.Components.SecuritySchemes.Add(
|
||||
nameof(GlobalConfig.IPCPassword), new OpenApiSecurityScheme {
|
||||
|
||||
@@ -28,7 +28,7 @@ using System.Threading.Tasks;
|
||||
using ArchiSteamFarm.Storage;
|
||||
using JetBrains.Annotations;
|
||||
using Microsoft.AspNetCore.OpenApi;
|
||||
using Microsoft.OpenApi.Models;
|
||||
using Microsoft.OpenApi;
|
||||
|
||||
namespace ArchiSteamFarm.IPC.OpenApi;
|
||||
|
||||
@@ -45,14 +45,8 @@ internal sealed class OperationTransformer : IOpenApiOperationTransformer {
|
||||
operation.Security.Add(
|
||||
new OpenApiSecurityRequirement {
|
||||
{
|
||||
new OpenApiSecurityScheme {
|
||||
Reference = new OpenApiReference {
|
||||
Id = nameof(GlobalConfig.IPCPassword),
|
||||
Type = ReferenceType.SecurityScheme
|
||||
}
|
||||
},
|
||||
|
||||
Array.Empty<string>()
|
||||
new OpenApiSecuritySchemeReference(nameof(GlobalConfig.IPCPassword), context.Document),
|
||||
[]
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
@@ -23,14 +23,13 @@
|
||||
|
||||
using System;
|
||||
using System.Globalization;
|
||||
using System.Text.Json.Nodes;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using ArchiSteamFarm.IPC.Integration;
|
||||
using JetBrains.Annotations;
|
||||
using Microsoft.AspNetCore.OpenApi;
|
||||
using Microsoft.OpenApi.Any;
|
||||
using Microsoft.OpenApi.Extensions;
|
||||
using Microsoft.OpenApi.Models;
|
||||
using Microsoft.OpenApi;
|
||||
|
||||
namespace ArchiSteamFarm.IPC.OpenApi;
|
||||
|
||||
@@ -72,9 +71,9 @@ internal sealed class SchemaTransformer : IOpenApiSchemaTransformer {
|
||||
schema.Format = "flags";
|
||||
}
|
||||
|
||||
OpenApiObject definition = new();
|
||||
JsonObject definition = new();
|
||||
|
||||
foreach (object? enumValue in context.JsonTypeInfo.Type.GetEnumValues()) {
|
||||
foreach (object? enumValue in context.JsonTypeInfo.Type.GetEnumValuesAsUnderlyingType()) {
|
||||
if (enumValue == null) {
|
||||
throw new InvalidOperationException(nameof(enumValue));
|
||||
}
|
||||
@@ -95,41 +94,26 @@ internal sealed class SchemaTransformer : IOpenApiSchemaTransformer {
|
||||
continue;
|
||||
}
|
||||
|
||||
IOpenApiAny enumObject;
|
||||
|
||||
if (TryCast(enumValue, out int intValue)) {
|
||||
enumObject = new OpenApiInteger(intValue);
|
||||
} else if (TryCast(enumValue, out long longValue)) {
|
||||
enumObject = new OpenApiLong(longValue);
|
||||
} else if (TryCast(enumValue, out ulong ulongValue)) {
|
||||
// OpenApi spec doesn't support ulongs as of now
|
||||
enumObject = new OpenApiString(ulongValue.ToString(CultureInfo.InvariantCulture));
|
||||
} else {
|
||||
throw new InvalidOperationException(nameof(enumValue));
|
||||
}
|
||||
|
||||
definition.Add(enumName, enumObject);
|
||||
// OpenApi seems to support only int and long from underlying enum types: https://learn.microsoft.com/dotnet/csharp/language-reference/builtin-types/integral-numeric-types
|
||||
definition[enumName] = enumValue switch {
|
||||
sbyte value => JsonValue.Create((int) value),
|
||||
byte value => JsonValue.Create((int) value),
|
||||
short value => JsonValue.Create((int) value),
|
||||
ushort value => JsonValue.Create((int) value),
|
||||
int value => JsonValue.Create(value),
|
||||
uint value => JsonValue.Create((long) value),
|
||||
long value => JsonValue.Create(value),
|
||||
ulong value => JsonValue.Create(value.ToString(CultureInfo.InvariantCulture)),
|
||||
nint value when nint.Size <= 4 => JsonValue.Create((int) value),
|
||||
nint value when nint.Size <= 8 => JsonValue.Create((long) value),
|
||||
nint value => JsonValue.Create(value.ToString(CultureInfo.InvariantCulture)),
|
||||
nuint value when nuint.Size <= 4 => JsonValue.Create((long) value),
|
||||
nuint value => JsonValue.Create(value.ToString(CultureInfo.InvariantCulture)),
|
||||
_ => throw new InvalidOperationException(nameof(enumValue))
|
||||
};
|
||||
}
|
||||
|
||||
schema.AddExtension("x-definition", definition);
|
||||
}
|
||||
|
||||
private static bool TryCast<T>(object value, out T typedValue) where T : struct {
|
||||
ArgumentNullException.ThrowIfNull(value);
|
||||
|
||||
try {
|
||||
typedValue = (T) Convert.ChangeType(value, typeof(T), CultureInfo.InvariantCulture);
|
||||
|
||||
return true;
|
||||
} catch (InvalidCastException) {
|
||||
typedValue = default(T);
|
||||
|
||||
return false;
|
||||
} catch (OverflowException) {
|
||||
typedValue = default(T);
|
||||
|
||||
return false;
|
||||
}
|
||||
schema.AddExtension("x-definition", new JsonNodeExtension(definition));
|
||||
}
|
||||
}
|
||||
#pragma warning restore CA1812 // False positive, the class is used internally
|
||||
|
||||
@@ -21,7 +21,8 @@
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
using System.Collections.Specialized;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.ComponentModel;
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
@@ -33,9 +34,10 @@ namespace ArchiSteamFarm.IPC.Requests;
|
||||
public sealed class BotGamesToRedeemInBackgroundRequest {
|
||||
[Description("A string-string map that maps cd-key to redeem (key) to its name (value). Key in the map must be a valid and unique Steam cd-key. Value in the map must be a non-null and non-empty name of the key (e.g. game's name, but can be anything)")]
|
||||
[JsonInclude]
|
||||
[JsonObjectCreationHandling(JsonObjectCreationHandling.Populate)]
|
||||
[JsonRequired]
|
||||
[Required]
|
||||
public OrderedDictionary GamesToRedeemInBackground { get; private init; } = new();
|
||||
public OrderedDictionary<string, string> GamesToRedeemInBackground { get; private init; } = new(StringComparer.OrdinalIgnoreCase);
|
||||
|
||||
[JsonConstructor]
|
||||
private BotGamesToRedeemInBackgroundRequest() { }
|
||||
|
||||
43
ArchiSteamFarm/IPC/Requests/BotRemoveLicenseRequest.cs
Normal file
43
ArchiSteamFarm/IPC/Requests/BotRemoveLicenseRequest.cs
Normal file
@@ -0,0 +1,43 @@
|
||||
// ----------------------------------------------------------------------------------------------
|
||||
// _ _ _ ____ _ _____
|
||||
// / \ _ __ ___ | |__ (_)/ ___| | |_ ___ __ _ _ __ ___ | ___|__ _ _ __ _ __ ___
|
||||
// / _ \ | '__|/ __|| '_ \ | |\___ \ | __|/ _ \ / _` || '_ ` _ \ | |_ / _` || '__|| '_ ` _ \
|
||||
// / ___ \ | | | (__ | | | || | ___) || |_| __/| (_| || | | | | || _|| (_| || | | | | | | |
|
||||
// /_/ \_\|_| \___||_| |_||_||____/ \__|\___| \__,_||_| |_| |_||_| \__,_||_| |_| |_| |_|
|
||||
// ----------------------------------------------------------------------------------------------
|
||||
// |
|
||||
// Copyright 2015-2025 Ł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.Collections.Immutable;
|
||||
using System.ComponentModel;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace ArchiSteamFarm.IPC.Requests;
|
||||
|
||||
[SuppressMessage("ReSharper", "ClassCannotBeInstantiated")]
|
||||
public sealed class BotRemoveLicenseRequest {
|
||||
[Description("A collection (set) of apps (appIDs) to remove license for")]
|
||||
[JsonInclude]
|
||||
public ImmutableList<uint>? Apps { get; private init; }
|
||||
|
||||
[Description("A collection (set) of packages (subIDs) to remove license for")]
|
||||
[JsonInclude]
|
||||
public ImmutableList<uint>? Packages { get; private init; }
|
||||
|
||||
[JsonConstructor]
|
||||
private BotRemoveLicenseRequest() { }
|
||||
}
|
||||
@@ -75,7 +75,6 @@ public sealed class ASFResponse {
|
||||
internal ASFResponse(string buildVariant, bool canUpdate, GlobalConfig globalConfig, uint memoryUsage, DateTime processStartTime, Version version) {
|
||||
ArgumentException.ThrowIfNullOrEmpty(buildVariant);
|
||||
ArgumentNullException.ThrowIfNull(globalConfig);
|
||||
ArgumentOutOfRangeException.ThrowIfZero(memoryUsage);
|
||||
ArgumentOutOfRangeException.ThrowIfLessThanOrEqual(processStartTime, DateTime.UnixEpoch);
|
||||
ArgumentNullException.ThrowIfNull(version);
|
||||
|
||||
|
||||
45
ArchiSteamFarm/IPC/Responses/BotInventoryResponse.cs
Normal file
45
ArchiSteamFarm/IPC/Responses/BotInventoryResponse.cs
Normal file
@@ -0,0 +1,45 @@
|
||||
// ----------------------------------------------------------------------------------------------
|
||||
// _ _ _ ____ _ _____
|
||||
// / \ _ __ ___ | |__ (_)/ ___| | |_ ___ __ _ _ __ ___ | ___|__ _ _ __ _ __ ___
|
||||
// / _ \ | '__|/ __|| '_ \ | |\___ \ | __|/ _ \ / _` || '_ ` _ \ | |_ / _` || '__|| '_ ` _ \
|
||||
// / ___ \ | | | (__ | | | || | ___) || |_| __/| (_| || | | | | || _|| (_| || | | | | | | |
|
||||
// /_/ \_\|_| \___||_| |_||_||____/ \__|\___| \__,_||_| |_| |_||_| \__,_||_| |_| |_| |_|
|
||||
// ----------------------------------------------------------------------------------------------
|
||||
// |
|
||||
// Copyright 2015-2025 Ł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.Collections.Generic;
|
||||
using System.Collections.Immutable;
|
||||
using System.ComponentModel;
|
||||
using System.Text.Json.Serialization;
|
||||
using SteamKit2.Internal;
|
||||
|
||||
namespace ArchiSteamFarm.IPC.Responses;
|
||||
|
||||
public sealed class BotInventoryResponse {
|
||||
[Description("Inventory assets")]
|
||||
[JsonInclude]
|
||||
public ImmutableHashSet<CEcon_Asset>? Assets { get; private init; }
|
||||
|
||||
[Description("Descriptions of the inventory assets")]
|
||||
[JsonInclude]
|
||||
public ImmutableHashSet<CEconItem_Description>? Descriptions { get; private init; }
|
||||
|
||||
internal BotInventoryResponse(IEnumerable<CEcon_Asset>? assets = null, IEnumerable<CEconItem_Description>? descriptions = null) {
|
||||
Assets = assets?.ToImmutableHashSet();
|
||||
Descriptions = descriptions?.ToImmutableHashSet();
|
||||
}
|
||||
}
|
||||
45
ArchiSteamFarm/IPC/Responses/BotRemoveLicenseResponse.cs
Normal file
45
ArchiSteamFarm/IPC/Responses/BotRemoveLicenseResponse.cs
Normal file
@@ -0,0 +1,45 @@
|
||||
// ----------------------------------------------------------------------------------------------
|
||||
// _ _ _ ____ _ _____
|
||||
// / \ _ __ ___ | |__ (_)/ ___| | |_ ___ __ _ _ __ ___ | ___|__ _ _ __ _ __ ___
|
||||
// / _ \ | '__|/ __|| '_ \ | |\___ \ | __|/ _ \ / _` || '_ ` _ \ | |_ / _` || '__|| '_ ` _ \
|
||||
// / ___ \ | | | (__ | | | || | ___) || |_| __/| (_| || | | | | || _|| (_| || | | | | | | |
|
||||
// /_/ \_\|_| \___||_| |_||_||____/ \__|\___| \__,_||_| |_| |_||_| \__,_||_| |_| |_| |_|
|
||||
// ----------------------------------------------------------------------------------------------
|
||||
// |
|
||||
// Copyright 2015-2025 Ł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.Collections.Generic;
|
||||
using System.Collections.Immutable;
|
||||
using System.ComponentModel;
|
||||
using System.Text.Json.Serialization;
|
||||
using SteamKit2;
|
||||
|
||||
namespace ArchiSteamFarm.IPC.Responses;
|
||||
|
||||
public sealed class BotRemoveLicenseResponse {
|
||||
[Description("A collection (set) of apps (appIDs) to ask license for")]
|
||||
[JsonInclude]
|
||||
public ImmutableDictionary<uint, EResult>? Apps { get; private init; }
|
||||
|
||||
[Description("A collection (set) of packages (subIDs) to ask license for")]
|
||||
[JsonInclude]
|
||||
public ImmutableDictionary<uint, EResult>? Packages { get; private init; }
|
||||
|
||||
internal BotRemoveLicenseResponse(IReadOnlyDictionary<uint, EResult>? apps, IReadOnlyDictionary<uint, EResult>? packages) {
|
||||
Apps = apps?.ToImmutableDictionary();
|
||||
Packages = packages?.ToImmutableDictionary();
|
||||
}
|
||||
}
|
||||
@@ -1,107 +0,0 @@
|
||||
// ----------------------------------------------------------------------------------------------
|
||||
// _ _ _ ____ _ _____
|
||||
// / \ _ __ ___ | |__ (_)/ ___| | |_ ___ __ _ _ __ ___ | ___|__ _ _ __ _ __ ___
|
||||
// / _ \ | '__|/ __|| '_ \ | |\___ \ | __|/ _ \ / _` || '_ ` _ \ | |_ / _` || '__|| '_ ` _ \
|
||||
// / ___ \ | | | (__ | | | || | ___) || |_| __/| (_| || | | | | || _|| (_| || | | | | | | |
|
||||
// /_/ \_\|_| \___||_| |_||_||____/ \__|\___| \__,_||_| |_| |_||_| \__,_||_| |_| |_| |_|
|
||||
// ----------------------------------------------------------------------------------------------
|
||||
// |
|
||||
// Copyright 2015-2025 Ł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.Globalization;
|
||||
using JetBrains.Annotations;
|
||||
using Microsoft.OpenApi.Any;
|
||||
using Microsoft.OpenApi.Extensions;
|
||||
using Microsoft.OpenApi.Models;
|
||||
using Swashbuckle.AspNetCore.SwaggerGen;
|
||||
|
||||
namespace ArchiSteamFarm.IPC.Swashbuckle;
|
||||
|
||||
[UsedImplicitly]
|
||||
internal sealed class EnumSchemaFilter : ISchemaFilter {
|
||||
public void Apply(OpenApiSchema schema, SchemaFilterContext context) {
|
||||
ArgumentNullException.ThrowIfNull(schema);
|
||||
ArgumentNullException.ThrowIfNull(context);
|
||||
|
||||
if (context.Type is not { IsEnum: true }) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (context.Type.IsDefined(typeof(FlagsAttribute), false)) {
|
||||
schema.Format = "flags";
|
||||
}
|
||||
|
||||
OpenApiObject definition = new();
|
||||
|
||||
foreach (object? enumValue in context.Type.GetEnumValues()) {
|
||||
if (enumValue == null) {
|
||||
throw new InvalidOperationException(nameof(enumValue));
|
||||
}
|
||||
|
||||
string? enumName = Enum.GetName(context.Type, enumValue);
|
||||
|
||||
if (string.IsNullOrEmpty(enumName)) {
|
||||
// Fallback
|
||||
enumName = enumValue.ToString();
|
||||
|
||||
if (string.IsNullOrEmpty(enumName)) {
|
||||
throw new InvalidOperationException(nameof(enumName));
|
||||
}
|
||||
}
|
||||
|
||||
if (definition.ContainsKey(enumName)) {
|
||||
// This is possible if we have multiple names for the same enum value, we'll ignore additional ones
|
||||
continue;
|
||||
}
|
||||
|
||||
IOpenApiPrimitive enumObject;
|
||||
|
||||
if (TryCast(enumValue, out int intValue)) {
|
||||
enumObject = new OpenApiInteger(intValue);
|
||||
} else if (TryCast(enumValue, out long longValue)) {
|
||||
enumObject = new OpenApiLong(longValue);
|
||||
} else if (TryCast(enumValue, out ulong ulongValue)) {
|
||||
// OpenApi spec doesn't support ulongs as of now
|
||||
enumObject = new OpenApiString(ulongValue.ToString(CultureInfo.InvariantCulture));
|
||||
} else {
|
||||
throw new InvalidOperationException(nameof(enumValue));
|
||||
}
|
||||
|
||||
definition.Add(enumName, enumObject);
|
||||
}
|
||||
|
||||
schema.AddExtension("x-definition", definition);
|
||||
}
|
||||
|
||||
private static bool TryCast<T>(object value, out T typedValue) where T : struct {
|
||||
ArgumentNullException.ThrowIfNull(value);
|
||||
|
||||
try {
|
||||
typedValue = (T) Convert.ChangeType(value, typeof(T), CultureInfo.InvariantCulture);
|
||||
|
||||
return true;
|
||||
} catch (InvalidCastException) {
|
||||
typedValue = default(T);
|
||||
|
||||
return false;
|
||||
} catch (OverflowException) {
|
||||
typedValue = default(T);
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -501,6 +501,7 @@ StackTrace:
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
<data name="PluginsWarning" xml:space="preserve">
|
||||
|
||||
@@ -578,6 +578,7 @@
|
||||
<value>Ботът има ниво {0}.</value>
|
||||
<comment>{0} will be replaced by bot's level</comment>
|
||||
</data>
|
||||
|
||||
<data name="ActivelyMatchingItems" xml:space="preserve">
|
||||
<value>Сравняват се Steam предмети, #{0} път...</value>
|
||||
<comment>{0} will be replaced by round number</comment>
|
||||
|
||||
@@ -380,6 +380,7 @@
|
||||
</data>
|
||||
|
||||
|
||||
|
||||
<data name="ErrorAborted" xml:space="preserve">
|
||||
<value>Obustavljeno!</value>
|
||||
</data>
|
||||
|
||||
@@ -83,7 +83,9 @@
|
||||
<value>{0} és invàlid!</value>
|
||||
<comment>{0} will be replaced by object's name</comment>
|
||||
</data>
|
||||
|
||||
<data name="ErrorNoBotsDefined" xml:space="preserve">
|
||||
<value>No s'ha definit cap agent. Potser t'ha passat per alt configurar l'ASF? Segueix la guia "posada en marxa" al wiki si tens dificultats.</value>
|
||||
</data>
|
||||
|
||||
|
||||
<data name="ErrorRequestFailedTooManyTimes" xml:space="preserve">
|
||||
@@ -148,8 +150,13 @@
|
||||
<data name="UpdateFinished" xml:space="preserve">
|
||||
<value>Procés d'actualització finalitzat!</value>
|
||||
</data>
|
||||
|
||||
|
||||
<data name="UpdateNewVersionAvailable" xml:space="preserve">
|
||||
<value>Disponible una nova versió de l'ASF! Sospesa una actualització!</value>
|
||||
</data>
|
||||
<data name="UpdateVersionInfo" xml:space="preserve">
|
||||
<value>Versió local: {0} | Versió remota: {1}</value>
|
||||
<comment>{0} will be replaced by current version, {1} will be replaced by remote version</comment>
|
||||
</data>
|
||||
|
||||
|
||||
|
||||
@@ -188,26 +195,64 @@
|
||||
<data name="CheckingOtherBadgePages" xml:space="preserve">
|
||||
<value>Comprovant les altres pàgines d'insígnies...</value>
|
||||
</data>
|
||||
|
||||
<data name="ChosenFarmingAlgorithm" xml:space="preserve">
|
||||
<value>Algorisme de cultiu escollit: {0}</value>
|
||||
<comment>{0} will be replaced by the name of chosen farming algorithm</comment>
|
||||
</data>
|
||||
<data name="Done" xml:space="preserve">
|
||||
<value>Fet!</value>
|
||||
</data>
|
||||
<data name="GamesToIdle" xml:space="preserve">
|
||||
<value>Hi ha un total de {0} jocs ({1} targetes) restants per cultivar (queden ~{2})...</value>
|
||||
<comment>{0} will be replaced by number of games, {1} will be replaced by number of cards, {2} will be replaced by translated TimeSpan string (such as "1 day, 5 hours and 30 minutes")</comment>
|
||||
</data>
|
||||
<data name="IdlingFinished" xml:space="preserve">
|
||||
<value>Cultiu finalitzat!</value>
|
||||
</data>
|
||||
<data name="IdlingFinishedForGame" xml:space="preserve">
|
||||
<value>Cultiu finalitzat: {0} ({1}) després de {2} de temps de joc!</value>
|
||||
<comment>{0} will be replaced by game's ID (number), {1} will be replaced by game's name, {2} will be replaced by translated TimeSpan string (such as "1 day, 5 hours and 30 minutes")</comment>
|
||||
</data>
|
||||
|
||||
<data name="IdlingStatusForGame" xml:space="preserve">
|
||||
<value>Estat del cultiu per {0} ({1}): Queden {2} targetes</value>
|
||||
<comment>{0} will be replaced by game's ID (number), {1} will be replaced by game's name, {2} will be replaced by number of cards left to farm</comment>
|
||||
</data>
|
||||
<data name="IdlingStopped" xml:space="preserve">
|
||||
<value>Cultiu aturat!</value>
|
||||
</data>
|
||||
|
||||
<data name="NothingToIdle" xml:space="preserve">
|
||||
<value>No hi ha res per cultivar en aquest compte!</value>
|
||||
</data>
|
||||
<data name="NowIdling" xml:space="preserve">
|
||||
<value>Cultivant en aquest moment: {0} ({1})</value>
|
||||
<comment>{0} will be replaced by game's ID (number), {1} will be replaced by game's name</comment>
|
||||
</data>
|
||||
<data name="NowIdlingList" xml:space="preserve">
|
||||
<value>Cultivant en aquest moment: {0}</value>
|
||||
<comment>{0} will be replaced by list of the games (IDs, numbers), separated by a comma</comment>
|
||||
</data>
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
<data name="StillIdling" xml:space="preserve">
|
||||
<value>Encara cultivant: {0} ({1})</value>
|
||||
<comment>{0} will be replaced by game's ID (number), {1} will be replaced by game's name</comment>
|
||||
</data>
|
||||
<data name="StillIdlingList" xml:space="preserve">
|
||||
<value>Encara cultivant: {0}</value>
|
||||
<comment>{0} will be replaced by list of the games (IDs, numbers), separated by a comma</comment>
|
||||
</data>
|
||||
<data name="StoppedIdling" xml:space="preserve">
|
||||
<value>Cultiu aturat: {0} ({1})</value>
|
||||
<comment>{0} will be replaced by game's ID (number), {1} will be replaced by game's name</comment>
|
||||
</data>
|
||||
<data name="StoppedIdlingList" xml:space="preserve">
|
||||
<value>Cultiu aturat: {0}</value>
|
||||
<comment>{0} will be replaced by list of the games (IDs, numbers), separated by a comma</comment>
|
||||
</data>
|
||||
<data name="UnknownCommand" xml:space="preserve">
|
||||
<value>Ordre desconeguda!</value>
|
||||
</data>
|
||||
|
||||
|
||||
|
||||
@@ -228,10 +273,18 @@
|
||||
</data>
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
<data name="BotAutomaticIdlingNowPaused" xml:space="preserve">
|
||||
<value>El cultiu automàtic està en pausa!</value>
|
||||
</data>
|
||||
<data name="BotAutomaticIdlingNowResumed" xml:space="preserve">
|
||||
<value>El cultiu automàtic s'ha reprès!</value>
|
||||
</data>
|
||||
<data name="BotAutomaticIdlingPausedAlready" xml:space="preserve">
|
||||
<value>El cultiu automàtic ja està en pausa!</value>
|
||||
</data>
|
||||
<data name="BotAutomaticIdlingResumedAlready" xml:space="preserve">
|
||||
<value>El cultiu automàtic ja s'ha reprès!</value>
|
||||
</data>
|
||||
<data name="BotConnected" xml:space="preserve">
|
||||
<value>Connectat a Steam!</value>
|
||||
</data>
|
||||
@@ -305,7 +358,10 @@
|
||||
|
||||
|
||||
|
||||
|
||||
<data name="TranslationIncomplete" xml:space="preserve">
|
||||
<value>L'ASF intentarà utilitzar el teu idioma preferit, {0}, però aquesta traducció està completa en un {1}. Potser t'agradaria ajudar a completar o millorar la traducció de l'ASF a la teva llengua?</value>
|
||||
<comment>{0} will be replaced by culture code, such as "en-US", {1} will be replaced by completeness percentage, such as "78.5%"</comment>
|
||||
</data>
|
||||
|
||||
|
||||
<data name="BotVersion" xml:space="preserve">
|
||||
@@ -335,20 +391,35 @@
|
||||
|
||||
|
||||
|
||||
<data name="PluginLoaded" xml:space="preserve">
|
||||
<value>{0} s'ha carregat correctament!</value>
|
||||
<comment>{0} will be replaced by the name of the custom ASF plugin</comment>
|
||||
</data>
|
||||
<data name="PluginLoading" xml:space="preserve">
|
||||
<value>Inicialitzant {0} V{1}...</value>
|
||||
<comment>{0} will be replaced by the name of the custom ASF plugin, {1} will be replaced by its version</comment>
|
||||
</data>
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
<data name="PleaseWait" xml:space="preserve">
|
||||
<value>Si us plau, espera...</value>
|
||||
</data>
|
||||
<data name="EnterCommand" xml:space="preserve">
|
||||
<value>Introdueix l'ordre: </value>
|
||||
</data>
|
||||
<data name="Executing" xml:space="preserve">
|
||||
<value>Executant...</value>
|
||||
</data>
|
||||
<data name="InteractiveConsoleEnabled" xml:space="preserve">
|
||||
<value>La consola interactiva ja està activa, prem "c" per entrar al mode d'introducció d'ordres.</value>
|
||||
</data>
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
<data name="UpdateCleanup" xml:space="preserve">
|
||||
<value>Fent neteja dels arxius antics després de l'actualització...</value>
|
||||
</data>
|
||||
|
||||
|
||||
|
||||
@@ -372,7 +443,9 @@
|
||||
|
||||
|
||||
|
||||
|
||||
<data name="PatchingFiles" xml:space="preserve">
|
||||
<value>Apedaçant els arxius de l'ASF...</value>
|
||||
</data>
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -584,6 +584,10 @@ StackTrace:
|
||||
<value>Bot má level {0}.</value>
|
||||
<comment>{0} will be replaced by bot's level</comment>
|
||||
</data>
|
||||
<data name="BotInventory" xml:space="preserve">
|
||||
<value>{0}/{1} ({2}/{3}): {4} aktiva</value>
|
||||
<comment>{0} will be replaced by appID (number), {1} will be replaced by contextID (number), {2} will be replaced by app's name (string), {3} will be replaced by name of the context (string), {4} will be replaced by number of assets in the specified inventory (number).</comment>
|
||||
</data>
|
||||
<data name="ActivelyMatchingItems" xml:space="preserve">
|
||||
<value>Srovnávám položky ze Steamu, kolo #{0}...</value>
|
||||
<comment>{0} will be replaced by round number</comment>
|
||||
@@ -792,5 +796,8 @@ StackTrace:
|
||||
<data name="CustomPluginUpdatesEnabled" xml:space="preserve">
|
||||
<value>Vlastní zásuvné moduly byly zaregistrovány pro automatické aktualizace. Tým ASF by vám rád připomněl, že pro vaši vlastní bezpečnost byste měli povolit automatické aktualizace pouze od důvěryhodných stran. Pokud jste to nechtěli udělat, můžete aktualizace zásuvných modulů zakázat v globální konfiguraci ASF.</value>
|
||||
</data>
|
||||
|
||||
<data name="Input" xml:space="preserve">
|
||||
<value>Input: {0}</value>
|
||||
<comment>{0} will be replaced by text input from the user.</comment>
|
||||
</data>
|
||||
</root>
|
||||
|
||||
@@ -584,6 +584,10 @@ Processens oppetid: {1}</value>
|
||||
<value>Bot har niveau {0}.</value>
|
||||
<comment>{0} will be replaced by bot's level</comment>
|
||||
</data>
|
||||
<data name="BotInventory" xml:space="preserve">
|
||||
<value>{0}/{1} ({2}/{3}): {4} aktiver</value>
|
||||
<comment>{0} will be replaced by appID (number), {1} will be replaced by contextID (number), {2} will be replaced by app's name (string), {3} will be replaced by name of the context (string), {4} will be replaced by number of assets in the specified inventory (number).</comment>
|
||||
</data>
|
||||
<data name="ActivelyMatchingItems" xml:space="preserve">
|
||||
<value>Matcher Steam inventar, runde #{0}...</value>
|
||||
<comment>{0} will be replaced by round number</comment>
|
||||
@@ -683,21 +687,35 @@ Processens oppetid: {1}</value>
|
||||
<value>Din krypteringsnøgle er for kort. Vi anbefaler at bruge en, der er mindst {0} bytes (tegn) lang.</value>
|
||||
<comment>{0} will be replaced by the number of bytes (characters) recommended</comment>
|
||||
</data>
|
||||
|
||||
|
||||
<data name="WarningDefaultCryptKeyUsedForHashing" xml:space="preserve">
|
||||
<value>Du bruger {0} indstilling af {1} ejendom, men du har ikke angivet en brugerdefineret --cryptkey. Du kan eventuelt give en brugerdefineret --cryptkey for øget sikkerhed, hvis du gerne vil.</value>
|
||||
<comment>{0} will be replaced by the name of a particular setting (e.g. "SCrypt"), {1} will be replaced by the name of the property (e.g. "IPCPassword")</comment>
|
||||
</data>
|
||||
<data name="WarningDefaultCryptKeyUsedForEncryption" xml:space="preserve">
|
||||
<value>Du bruger {0} indstilling af {1} ejendom, men du har ikke angivet en brugerdefineret --cryptkey. Dette går helt ud over beskyttelsen, da ASF er tvunget til at bruge sin egen (kendte) nøgle. Du bør give en brugerdefineret --cryptkey til at gøre brug af den sikkerhedsmæssige fordel, som denne indstilling.</value>
|
||||
<comment>{0} will be replaced by the name of a particular setting (e.g. "AES"), {1} will be replaced by the name of the property (e.g. "SteamPassword")</comment>
|
||||
</data>
|
||||
<data name="WarningRunningAsRoot" xml:space="preserve">
|
||||
<value>Du forsøger at køre ASF som administrator (root). Dette medfører en betydelig sikkerhedsrisiko for din maskine, og da ASF ikke kræver root-adgang for dens drift, vi anbefaler at køre det som ikke-administrator bruger hvis det er muligt.</value>
|
||||
</data>
|
||||
|
||||
<data name="WarningRunningInUnsupportedEnvironment" xml:space="preserve">
|
||||
<value>Du kører ASF i ikke-understøttet miljø, leverer --ignore-unsupported-environment argument. Bemærk, at vi ikke tilbyder nogen form for støtte til dette scenarie, og du gør det helt på egen risiko. Du er blevet advaret.</value>
|
||||
</data>
|
||||
<data name="FetchingChecksumFromRemoteServer" xml:space="preserve">
|
||||
<value>Henter checksum fra den eksterne server...</value>
|
||||
</data>
|
||||
<data name="VerifyingChecksumWithRemoteServer" xml:space="preserve">
|
||||
<value>Verificerer checksum af den downloadede binary mod den fra den eksterne server...</value>
|
||||
</data>
|
||||
|
||||
|
||||
|
||||
<data name="ChecksumMissing" xml:space="preserve">
|
||||
<value>Fjernserveren ved ikke noget om den udgivelse, vi opdaterer til. Denne situation er mulig, hvis udgivelsen blev offentliggjort for nylig - nægter at gå videre med opdateringsproceduren med det samme som en yderligere sikkerhedsforanstaltning.</value>
|
||||
</data>
|
||||
<data name="ChecksumTimeout" xml:space="preserve">
|
||||
<value>Kunne ikke hente checksum af den downloadede binære - nægter at fortsætte med opdateringsproceduren på dette tidspunkt som en ekstra sikkerhedsforanstaltning.</value>
|
||||
</data>
|
||||
<data name="ChecksumWrong" xml:space="preserve">
|
||||
<value>Fjernserver har svaret med en anden tjeksum, dette kan indikere beskadiget download eller MITM angreb, nægter at fortsætte med opdateringsproceduren!</value>
|
||||
</data>
|
||||
<data name="PatchingFiles" xml:space="preserve">
|
||||
<value>Retter ASF-filer...</value>
|
||||
</data>
|
||||
@@ -717,8 +735,13 @@ Processens oppetid: {1}</value>
|
||||
<value>ASF kan ikke bruge app {0}, da den har regionsrelateret begrænsning for {1} der varer indtil {2}.</value>
|
||||
<comment>{0} will be replaced by app ID (number), {1} will be replaced by short country code (string, such as "PL"), {2} will be replaced by human-readable date (string).</comment>
|
||||
</data>
|
||||
|
||||
|
||||
<data name="WarningUnsupportedOfficialPlugins" xml:space="preserve">
|
||||
<value>Du forsøger at køre officielle {0} plugin i forkert matchet med ASF version: {1} (forventet {2}). Dette tyder på, at du gør noget forfærdeligt forkert, enten lave din opsætning eller forsyning - ignorer-ikke-understøttet miljø-argument, hvis du virkelig ved, hvad du laver.</value>
|
||||
<comment>{0} will be replaced by plugin name, {1} will be replaced by plugin's version number, {2} will be replaced by ASF's version number.</comment>
|
||||
</data>
|
||||
<data name="ErrorTooManyCrashes" xml:space="preserve">
|
||||
<value>Din ASF er styrtet for mange gange for nylig, og på grund af at processen initialisering er blevet deaktiveret. Enten undersøge, rette din opsætning, og fjern derefter ASF. udslæt fil fra din config mappe, eller forsyning -- ignore-unsupported-environment argument hvis du virkelig ved, hvad du laver.</value>
|
||||
</data>
|
||||
<data name="IdlingGameNotPossiblePrivate" xml:space="preserve">
|
||||
<value>Idling {0} ({1}) er deaktiveret, da spillet i øjeblikket er markeret som privat. Hvis du har til hensigt fra ASF til at idle det spil, så overvej at ændre dine privatlivsindstillinger.</value>
|
||||
<comment>{0} will be replaced by game's ID (number), {1} will be replaced by game's name</comment>
|
||||
@@ -746,8 +769,14 @@ Processens oppetid: {1}</value>
|
||||
<value>Fandt {0} plugin opdatering fra version {1} til {2}...</value>
|
||||
<comment>{0} will be replaced by plugin name (string), {1} will be replaced by current plugin's version, {2} will be replaced by remote plugin's version.</comment>
|
||||
</data>
|
||||
|
||||
|
||||
<data name="PluginUpdateNoAssetFound" xml:space="preserve">
|
||||
<value>Ingen aktiv tilgængelig for {0} plugin opdatering fra version {1} til {2}, dette betyder normalt, at opdateringen vil være tilgængelig på senere tidspunkt.</value>
|
||||
<comment>{0} will be replaced by plugin name (string), {1} will be replaced by current plugin's version, {2} will be replaced by remote plugin's version.</comment>
|
||||
</data>
|
||||
<data name="PluginUpdateConflictingAssetsFound" xml:space="preserve">
|
||||
<value>Intet aktiv kunne bestemmes for {0} plugin opdatering fra version {1} til {2}. Dette kan ske, hvis udgivelsen ikke er færdig endnu - hvis det bliver ved med at ske, bør du underrette plugin skaberen om det.</value>
|
||||
<comment>{0} will be replaced by plugin name (string), {1} will be replaced by current plugin's version, {2} will be replaced by remote plugin's version.</comment>
|
||||
</data>
|
||||
<data name="PluginUpdateInProgress" xml:space="preserve">
|
||||
<value>Opdaterer {0} plugin...</value>
|
||||
<comment>{0} will be replaced by plugin name (string).</comment>
|
||||
@@ -764,6 +793,11 @@ Processens oppetid: {1}</value>
|
||||
<value>{0} ({1}) plugin er blevet deaktiveret fra automatiske opdateringer, på trods af at den understøtter den funktion.</value>
|
||||
<comment>{0} will be replaced by plugin name (string), {1} will be replaced by plugin assembly name (string).</comment>
|
||||
</data>
|
||||
|
||||
|
||||
<data name="CustomPluginUpdatesEnabled" xml:space="preserve">
|
||||
<value>Tilpassede plugins er blevet registreret til automatiske opdateringer. ASF team vil gerne minde dig om, at for din egen sikkerhed, bør du kun aktivere automatiske opdateringer fra betroede parter. Hvis du ikke har til hensigt at gøre dette, kan du deaktivere plugin opdateringer i ASF global konfiguration.</value>
|
||||
</data>
|
||||
<data name="Input" xml:space="preserve">
|
||||
<value>Input: {0}</value>
|
||||
<comment>{0} will be replaced by text input from the user.</comment>
|
||||
</data>
|
||||
</root>
|
||||
|
||||
@@ -206,7 +206,7 @@ StackTrace:
|
||||
<comment>Please note that this translation should end with space</comment>
|
||||
</data>
|
||||
<data name="UserInputSteamParentalCode" xml:space="preserve">
|
||||
<value>Bitte geben Sie Ihre Steam-Familienansicht-PIN ein: </value>
|
||||
<value>Bitte geben Sie Ihre Steam-Familienansicht (PIN) ein: </value>
|
||||
<comment>Please note that this translation should end with space</comment>
|
||||
</data>
|
||||
<data name="UserInputSteamPassword" xml:space="preserve">
|
||||
@@ -545,7 +545,7 @@ StackTrace:
|
||||
<value>Zugriff verweigert!</value>
|
||||
</data>
|
||||
<data name="WarningPreReleaseVersion" xml:space="preserve">
|
||||
<value>Sie verwenden eine Version, die neuer ist als die zuletzt veröffentlichte Version Ihres Aktualisierungskanals. Bitte bedenken Sie, dass Vorabversionen nur für Benutzer gedacht sind, die wissen wie man Fehler meldet, mit Problemen umgeht und Rückmeldung gibt – es wird keine technische Unterstützung geben.</value>
|
||||
<value>Sie verwenden eine Version, die neuer ist als die zuletzt veröffentlichte Version Ihres Aktualisierungskanals. Bitte bedenken Sie, dass Vorabversionen nur an Benutzer gerichtet sind, die bereit sind, Fehlern korrek zu melden, mit Problemen umgehen und Rückmeldungen geben – es wird keine technische Unterstützung geben.</value>
|
||||
</data>
|
||||
<data name="BotStats" xml:space="preserve">
|
||||
<value>Aktuelle Speichernutzung: {0} MB.
|
||||
@@ -585,6 +585,10 @@ Prozesslaufzeit: {1}</value>
|
||||
<value>Der Bot hat das Level {0}.</value>
|
||||
<comment>{0} will be replaced by bot's level</comment>
|
||||
</data>
|
||||
<data name="BotInventory" xml:space="preserve">
|
||||
<value>{0}/{1} ({2}/{3}): {4} Gegenstände</value>
|
||||
<comment>{0} will be replaced by appID (number), {1} will be replaced by contextID (number), {2} will be replaced by app's name (string), {3} will be replaced by name of the context (string), {4} will be replaced by number of assets in the specified inventory (number).</comment>
|
||||
</data>
|
||||
<data name="ActivelyMatchingItems" xml:space="preserve">
|
||||
<value>Vergleiche Steam-Gegenstände, Durchgang #{0}...</value>
|
||||
<comment>{0} will be replaced by round number</comment>
|
||||
@@ -759,7 +763,7 @@ Prozesslaufzeit: {1}</value>
|
||||
<comment>{0} will be replaced by plugin name (string), {1} will be replaced by current plugin's version, {2} will be replaced by remote plugin's version.</comment>
|
||||
</data>
|
||||
<data name="PluginUpdateNewVersionAvailable" xml:space="preserve">
|
||||
<value>Es ist eine neue ASF-Version verfügbar! Erwägen Sie, das Programm selbst zu aktualisieren!</value>
|
||||
<value>Es ist eine neue Plugin-Version verfügbar! Erwägen Sie, dieses selbst zu aktualisieren!</value>
|
||||
<comment>{0} will be replaced by plugin name (string).</comment>
|
||||
</data>
|
||||
<data name="PluginUpdateFound" xml:space="preserve">
|
||||
@@ -793,5 +797,8 @@ Prozesslaufzeit: {1}</value>
|
||||
<data name="CustomPluginUpdatesEnabled" xml:space="preserve">
|
||||
<value>Benutzerdefinierte Erweiterungen wurden für automatische Updates registriert. Das ASF-Team möchte Sie daran erinnern, dass Sie zu Ihrer eigenen Sicherheit automatische Updates nur von vertrauenswürdigen Parteien aktivieren sollten. Wenn Sie dies nicht beabsichtigen, können Sie Plugin-Updates in der globalen ASF-Konfuguration deaktivieren.</value>
|
||||
</data>
|
||||
|
||||
<data name="Input" xml:space="preserve">
|
||||
<value>Eingabe: {0}</value>
|
||||
<comment>{0} will be replaced by text input from the user.</comment>
|
||||
</data>
|
||||
</root>
|
||||
|
||||
@@ -584,6 +584,10 @@ StackTrace:
|
||||
<value>Το bot βρίσκεται στο επίπεδο {0}.</value>
|
||||
<comment>{0} will be replaced by bot's level</comment>
|
||||
</data>
|
||||
<data name="BotInventory" xml:space="preserve">
|
||||
<value>{0}/{1} ({2}/{3}): {4} assets</value>
|
||||
<comment>{0} will be replaced by appID (number), {1} will be replaced by contextID (number), {2} will be replaced by app's name (string), {3} will be replaced by name of the context (string), {4} will be replaced by number of assets in the specified inventory (number).</comment>
|
||||
</data>
|
||||
<data name="ActivelyMatchingItems" xml:space="preserve">
|
||||
<value>Ταίριασμα στοιχείων Steam, γύρος #{0}...</value>
|
||||
<comment>{0} will be replaced by round number</comment>
|
||||
@@ -706,7 +710,9 @@ StackTrace:
|
||||
<data name="ChecksumMissing" xml:space="preserve">
|
||||
<value>Ο απομακρυσμένος διακομιστής δε γνωρίζει τίποτα σχετικά με την έκδοση στην οποία ενημερώνουμε. Αυτή η κατάσταση είναι πιθανή εάν η νέα έκδοση δημοσιεύτηκε πρόσφατα - Ως πρόσθετο μέτρο ασφαλείας το πρόγραμμα δε θα προβεί στην άμεση διαδικασία ενημέρωσης.</value>
|
||||
</data>
|
||||
|
||||
<data name="ChecksumTimeout" xml:space="preserve">
|
||||
<value>Αποτυχία λήψης του αθροίσματος ελέγχου του ληφθέντος δυαδικού - άρνηση να προχωρήσει με τη διαδικασία ενημέρωσης αυτή τη στιγμή ως πρόσθετο μέτρο ασφαλείας.</value>
|
||||
</data>
|
||||
<data name="ChecksumWrong" xml:space="preserve">
|
||||
<value>Ο απομακρυσμένος διακομιστής απάντησε με ένα διαφορετικό checksum, αυτό μπορεί να υποδεικνύει κατεστραμμένο download ή MITM attack, αρνούμενος να προχωρήσει με τη διαδικασία ενημέρωσης!</value>
|
||||
</data>
|
||||
@@ -729,9 +735,17 @@ StackTrace:
|
||||
<value>Το ASF δεν είναι σε θέση να εκτελέσει την εφαρμογή {0} λόγω γεωγραφικού περιορισμού ως προς τη χώρα {1} που διαρκεί έως {2}.</value>
|
||||
<comment>{0} will be replaced by app ID (number), {1} will be replaced by short country code (string, such as "PL"), {2} will be replaced by human-readable date (string).</comment>
|
||||
</data>
|
||||
|
||||
|
||||
|
||||
<data name="WarningUnsupportedOfficialPlugins" xml:space="preserve">
|
||||
<value>Προσπαθείτε να εκτελέσετε επίσημο πρόσθετο {0} σε αναντιστοιχία με την έκδοση ASF: {1} (αναμένεται {2}). Αυτό υποδηλώνει ότι κάνετε κάτι τρομερά λάθος, είτε διορθώσετε τις ρυθμίσεις σας είτε παρέχετε την επιλογή --ignore-unsupported-environment αν πραγματικά ξέρετε τι κάνετε.</value>
|
||||
<comment>{0} will be replaced by plugin name, {1} will be replaced by plugin's version number, {2} will be replaced by ASF's version number.</comment>
|
||||
</data>
|
||||
<data name="ErrorTooManyCrashes" xml:space="preserve">
|
||||
<value>Το ASF σας συνετρίβη πάρα πολλές φορές πρόσφατα και λόγω αυτού η έναρξη της διαδικασίας έχει απενεργοποιηθεί. Είτε διερευνήσετε, να διορθώσετε την εγκατάστασή σας και, στη συνέχεια, αφαιρέστε το ASF. rash file from your config directory, or supply --ignore-unsupported-environment argument if you really know what you're doing .</value>
|
||||
</data>
|
||||
<data name="IdlingGameNotPossiblePrivate" xml:space="preserve">
|
||||
<value>Η γεωργία {0} ({1}) είναι απενεργοποιημένη, καθώς το παιχνίδι αυτή τη στιγμή επισημαίνεται ως ιδιωτικό. Εάν προτίθεστε από το ASF στο αγρόκτημα αυτό το παιχνίδι, τότε εξετάστε το ενδεχόμενο αλλαγής των ρυθμίσεων απορρήτου του.</value>
|
||||
<comment>{0} will be replaced by game's ID (number), {1} will be replaced by game's name</comment>
|
||||
</data>
|
||||
<data name="WarningSkipping" xml:space="preserve">
|
||||
<value>Παράλειψη: {0}...</value>
|
||||
<comment>{0} will be replaced by text value (string) of entry being skipped.</comment>
|
||||
@@ -747,14 +761,35 @@ StackTrace:
|
||||
<value>Δεν υπάρχει διαθέσιμη ενημέρωση για το πρόσθετο {0}: {1} ≥ {2}.</value>
|
||||
<comment>{0} will be replaced by plugin name (string), {1} will be replaced by current plugin's version, {2} will be replaced by remote plugin's version.</comment>
|
||||
</data>
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
<data name="PluginUpdateNewVersionAvailable" xml:space="preserve">
|
||||
<value>Νέα έκδοση {0} plugin είναι διαθέσιμη! Σκεφτείτε να ενημερώσετε τον εαυτό σας!</value>
|
||||
<comment>{0} will be replaced by plugin name (string).</comment>
|
||||
</data>
|
||||
<data name="PluginUpdateFound" xml:space="preserve">
|
||||
<value>Βρέθηκε ενημέρωση του πρόσθετου {0} από την έκδοση {1} σε {2}...</value>
|
||||
<comment>{0} will be replaced by plugin name (string), {1} will be replaced by current plugin's version, {2} will be replaced by remote plugin's version.</comment>
|
||||
</data>
|
||||
<data name="PluginUpdateInProgress" xml:space="preserve">
|
||||
<value>Ενημέρωση του πρόσθετου {0}...</value>
|
||||
<comment>{0} will be replaced by plugin name (string).</comment>
|
||||
</data>
|
||||
<data name="PluginUpdateFinished" xml:space="preserve">
|
||||
<value>Επιτυχής ενημέρωση του {0} plugin, οι αλλαγές θα φορτωθούν στην επόμενη εκκίνηση του ASF.</value>
|
||||
<comment>{0} will be replaced by plugin name (string).</comment>
|
||||
</data>
|
||||
<data name="PluginUpdateEnabled" xml:space="preserve">
|
||||
<value>{0}/{1} plugin έχει καταχωρηθεί και ενεργοποιηθεί για αυτόματες ενημερώσεις.</value>
|
||||
<comment>{0} will be replaced by plugin name (string), {1} will be replaced by plugin assembly name (string).</comment>
|
||||
</data>
|
||||
<data name="PluginUpdateDisabled" xml:space="preserve">
|
||||
<value>{0} ({1}) plugin έχει απενεργοποιηθεί από αυτόματες ενημερώσεις, παρά την υποστήριξη αυτής της δυνατότητας.</value>
|
||||
<comment>{0} will be replaced by plugin name (string), {1} will be replaced by plugin assembly name (string).</comment>
|
||||
</data>
|
||||
<data name="CustomPluginUpdatesEnabled" xml:space="preserve">
|
||||
<value>Τα προσαρμοσμένα πρόσθετα έχουν καταχωρηθεί για αυτόματες ενημερώσεις. Ομάδα ASF θα ήθελε να σας υπενθυμίσει ότι, για τη δική σας ασφάλεια, θα πρέπει να ενεργοποιήσετε την αυτόματη ενημέρωση μόνο από αξιόπιστα μέρη. Αν δεν προτίθεστε να το κάνετε αυτό, μπορείτε να απενεργοποιήσετε ενημερώσεις plugin σε γενικές ρυθμίσεις ASF.</value>
|
||||
</data>
|
||||
<data name="Input" xml:space="preserve">
|
||||
<value>Input: {0}</value>
|
||||
<comment>{0} will be replaced by text input from the user.</comment>
|
||||
</data>
|
||||
</root>
|
||||
|
||||
@@ -583,6 +583,10 @@ Tiempo de actividad del proceso: {1}</value>
|
||||
<value>El bot tiene nivel {0}.</value>
|
||||
<comment>{0} will be replaced by bot's level</comment>
|
||||
</data>
|
||||
<data name="BotInventory" xml:space="preserve">
|
||||
<value>{0}/{1} ({2}/{3}): {4} elementos</value>
|
||||
<comment>{0} will be replaced by appID (number), {1} will be replaced by contextID (number), {2} will be replaced by app's name (string), {3} will be replaced by name of the context (string), {4} will be replaced by number of assets in the specified inventory (number).</comment>
|
||||
</data>
|
||||
<data name="ActivelyMatchingItems" xml:space="preserve">
|
||||
<value>Emparejando artículos de Steam, ronda #{0}...</value>
|
||||
<comment>{0} will be replaced by round number</comment>
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user