mirror of
https://github.com/DoctorMcKay/node-steamcommunity.git
synced 2026-08-19 21:23:28 +08:00
Compare commits
622 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4ec372b44b | ||
|
|
25dfa2cdd2 | ||
|
|
c006526f69 | ||
|
|
8e1f214612 | ||
|
|
38d3fb39bf | ||
|
|
e65d50b5f7 | ||
|
|
3a65768b3f | ||
|
|
96e3e7ebc0 | ||
|
|
e9bc778e2a | ||
|
|
c1901d5f55 | ||
|
|
028ee43bda | ||
|
|
0ba889ee7e | ||
|
|
ccf16b64e6 | ||
|
|
078e39584e | ||
|
|
2094f0545e | ||
|
|
b48e8fa537 | ||
|
|
2bfaecd88f | ||
|
|
f776d9fd4a | ||
|
|
86e87e88ed | ||
|
|
f69519f547 | ||
|
|
29e18806cc | ||
|
|
c855b505c5 | ||
|
|
6cc96b51fa | ||
|
|
edf2891800 | ||
|
|
0fc1120fd3 | ||
|
|
a0aee350cc | ||
|
|
2abc547f49 | ||
|
|
b2bffddfbe | ||
|
|
ab9baeafc6 | ||
|
|
788f34ba7a | ||
|
|
bde2cafaf4 | ||
|
|
36e8c79d87 | ||
|
|
4ca0fc83aa | ||
|
|
2dd5bb490e | ||
|
|
d0da148d6b | ||
|
|
7605dc8a41 | ||
|
|
5f46aee9fc | ||
|
|
02cc5daafe | ||
|
|
e76d34891a | ||
|
|
20ad260d68 | ||
|
|
76f2c55a73 | ||
|
|
77c2811e7e | ||
|
|
37bac4d240 | ||
|
|
d23d7e22c0 | ||
|
|
6e19ffd93b | ||
|
|
e5fbddc00c | ||
|
|
3531560083 | ||
|
|
6fa6a073a8 | ||
|
|
fd872490c8 | ||
|
|
be528d1a19 | ||
|
|
440b2f9ba9 | ||
|
|
e23fa02c91 | ||
|
|
33bc8d83c9 | ||
|
|
f96d25ad52 | ||
|
|
f7d7cf0660 | ||
|
|
27d2daac61 | ||
|
|
4723bd93a6 | ||
|
|
897ad16154 | ||
|
|
c25cb31e34 | ||
|
|
81d8dc2a5c | ||
|
|
8538589fef | ||
|
|
c0be17cd61 | ||
|
|
350f6288e4 | ||
|
|
ba2782066d | ||
|
|
d6b0fbd5d0 | ||
|
|
9ec5dcd7b0 | ||
|
|
c8642865a1 | ||
|
|
c92801d25e | ||
|
|
b55516f67f | ||
|
|
f09adc84a4 | ||
|
|
d807f10653 | ||
|
|
18011b3fac | ||
|
|
6f97370710 | ||
|
|
711ab8a3ae | ||
|
|
9218ec6dd9 | ||
|
|
d28c2cdde9 | ||
|
|
b98997a595 | ||
|
|
2a1d244114 | ||
|
|
3bcaf40850 | ||
|
|
0ccac69bea | ||
|
|
48d63857e2 | ||
|
|
b11734cd9b | ||
|
|
5c37c38dda | ||
|
|
260bfaa0ad | ||
|
|
1031a4dbd7 | ||
|
|
e28fe5ca05 | ||
|
|
90a9ee1aa9 | ||
|
|
7ebbd5806f | ||
|
|
9a069e80fb | ||
|
|
640c47b933 | ||
|
|
f5d65a9ad4 | ||
|
|
43c0504232 | ||
|
|
48f0637c18 | ||
|
|
ad095605b6 | ||
|
|
d09f18f055 | ||
|
|
c7a8c676b7 | ||
|
|
fd5ed31afa | ||
|
|
e2ef03cc32 | ||
|
|
e49c5c175e | ||
|
|
565132d1bc | ||
|
|
7c6e531dbd | ||
|
|
ac19e0867b | ||
|
|
e9d3221ee6 | ||
|
|
dfda22cee4 | ||
|
|
485529e807 | ||
|
|
3def387d56 | ||
|
|
605fb6268f | ||
|
|
89d058fc99 | ||
|
|
9d47d28a1e | ||
|
|
40ee35ac89 | ||
|
|
e252efc455 | ||
|
|
a4de664759 | ||
|
|
59df4dee1d | ||
|
|
ca887c46ba | ||
|
|
8c0994cec3 | ||
|
|
9ec329569f | ||
|
|
600a41143b | ||
|
|
4c59309eb4 | ||
|
|
2adc23f0ac | ||
|
|
5b40683873 | ||
|
|
7ae8a97a0f | ||
|
|
63b0b82531 | ||
|
|
2b678bea06 | ||
|
|
8ffa0b50d4 | ||
|
|
d093466d27 | ||
|
|
c69a5451eb | ||
|
|
9a4be453ec | ||
|
|
c550b380fc | ||
|
|
247d804c77 | ||
|
|
dfd0ccbbc7 | ||
|
|
ec1284e961 | ||
|
|
dc4bb1b554 | ||
|
|
cd90d7a563 | ||
|
|
851c14bd93 | ||
|
|
24a42e9658 | ||
|
|
8ec671d952 | ||
|
|
2821a5900c | ||
|
|
2da6725283 | ||
|
|
23a7940064 | ||
|
|
165c963b1a | ||
|
|
429a76edcc | ||
|
|
3a224ff45f | ||
|
|
dfad525e4e | ||
|
|
81877f4bb7 | ||
|
|
f6a3186865 | ||
|
|
e0687f8828 | ||
|
|
d54dd2c892 | ||
|
|
b4818d6d67 | ||
|
|
d8eb07f046 | ||
|
|
e6995e337e | ||
|
|
2bbdbf36ad | ||
|
|
e6311c0fcb | ||
|
|
2fc8d1f4aa | ||
|
|
b00b871a5b | ||
|
|
15079621d2 | ||
|
|
d91c600eae | ||
|
|
0f98403c16 | ||
|
|
d9b6b55efc | ||
|
|
0fc62d2966 | ||
|
|
5daf5f712a | ||
|
|
737aa9dc41 | ||
|
|
07fb26047b | ||
|
|
2f9b24c447 | ||
|
|
211c625da0 | ||
|
|
2e61a6ed70 | ||
|
|
f0a3cb27f8 | ||
|
|
6ed33a100e | ||
|
|
d41a52638b | ||
|
|
58516d4ed1 | ||
|
|
3f99c718d4 | ||
|
|
b92c5d852d | ||
|
|
f5da011cde | ||
|
|
3633ab032c | ||
|
|
76df74d724 | ||
|
|
8b5cd59021 | ||
|
|
25adcb101b | ||
|
|
d7ecad670d | ||
|
|
8225ea5127 | ||
|
|
59bf4f1e92 | ||
|
|
a3468bf99b | ||
|
|
e299623452 | ||
|
|
29b31009ce | ||
|
|
fa253920e3 | ||
|
|
30ebf333b4 | ||
|
|
d7cfc396a7 | ||
|
|
ac8e4a62de | ||
|
|
8e9e3723f2 | ||
|
|
6b0f958cfd | ||
|
|
d4d0d7da26 | ||
|
|
3c8488d441 | ||
|
|
3ea0995415 | ||
|
|
21c4ba3e27 | ||
|
|
4d4df7c2a5 | ||
|
|
aa0d4d32b7 | ||
|
|
444a6d238d | ||
|
|
98373708e5 | ||
|
|
0df2662053 | ||
|
|
a46ed17415 | ||
|
|
9ed377e843 | ||
|
|
868ce9cdb4 | ||
|
|
8edc5020e1 | ||
|
|
f2be027a63 | ||
|
|
8a7d6f5018 | ||
|
|
fbfff1398b | ||
|
|
0cb0d22433 | ||
|
|
103e941501 | ||
|
|
f99d86aa73 | ||
|
|
5a855316f6 | ||
|
|
729f078a25 | ||
|
|
80dbf9d688 | ||
|
|
224b7b85e0 | ||
|
|
22d34b1faf | ||
|
|
cfd932dc72 | ||
|
|
4710afa28b | ||
|
|
1180eb9d3f | ||
|
|
3ca85bedc2 | ||
|
|
e05efabcb9 | ||
|
|
6cd06c3adf | ||
|
|
d35e3c107c | ||
|
|
4e90811008 | ||
|
|
018d3de5a2 | ||
|
|
3949abe09f | ||
|
|
62394b9ffe | ||
|
|
ebac6c97ad | ||
|
|
a0ad33d5eb | ||
|
|
b7a3e51bfc | ||
|
|
bf13224cbf | ||
|
|
117374a4c5 | ||
|
|
aa84505132 | ||
|
|
bb97565f2a | ||
|
|
7462e4b767 | ||
|
|
adbf1d47a7 | ||
|
|
8c4fde720a | ||
|
|
458d7b6db1 | ||
|
|
411d74036f | ||
|
|
1b3117b8dc | ||
|
|
6eec88d196 | ||
|
|
3921ce17d0 | ||
|
|
4441d6f6e0 | ||
|
|
870de6817e | ||
|
|
d5dbb230df | ||
|
|
5e9c06bdc4 | ||
|
|
8d163d0d85 | ||
|
|
051d376508 | ||
|
|
913dff68ef | ||
|
|
754847ee88 | ||
|
|
645e03b89c | ||
|
|
aa302bc09e | ||
|
|
1c0f8d4bca | ||
|
|
a7bd694f4b | ||
|
|
9e380e71f5 | ||
|
|
e97e63fe6d | ||
|
|
f6c7c74ab6 | ||
|
|
8846082f60 | ||
|
|
eb609b5c42 | ||
|
|
238a36de0f | ||
|
|
c38111b61a | ||
|
|
0eb27a15d2 | ||
|
|
0ab3a94493 | ||
|
|
7c2ec65b8c | ||
|
|
8e761ffe6c | ||
|
|
06f04887fd | ||
|
|
e5f986a8bf | ||
|
|
5da6e40ebb | ||
|
|
0b8dd57a40 | ||
|
|
6f8456882a | ||
|
|
0b57b47ecb | ||
|
|
b5f50a6614 | ||
|
|
4257140c15 | ||
|
|
724badd175 | ||
|
|
a9455b0ff6 | ||
|
|
b3dfdd47d8 | ||
|
|
1efac6bc70 | ||
|
|
eacb85c711 | ||
|
|
8eab34bf18 | ||
|
|
b45e8d97ed | ||
|
|
6abb14a9bc | ||
|
|
6049d7a259 | ||
|
|
e0320b27d7 | ||
|
|
7986a87b3f | ||
|
|
cf7e5f8995 | ||
|
|
a03dad9507 | ||
|
|
7da7ecfc56 | ||
|
|
6bf3f26f2f | ||
|
|
dc2e5d09a7 | ||
|
|
d716e8a494 | ||
|
|
458a179852 | ||
|
|
f5d76527ba | ||
|
|
5875f47ebc | ||
|
|
d7095aa7c5 | ||
|
|
03e0a1bdac | ||
|
|
a51973c9b9 | ||
|
|
96d87ee8e6 | ||
|
|
7beef548dc | ||
|
|
6d9ebabb17 | ||
|
|
5d5d8548af | ||
|
|
b5142ad0cc | ||
|
|
3045a5d0cc | ||
|
|
2dcb123761 | ||
|
|
bd29d598b1 | ||
|
|
7e39715427 | ||
|
|
a97edfd492 | ||
|
|
68b1f21655 | ||
|
|
b4772baf39 | ||
|
|
47dc22ca0d | ||
|
|
3bba7025d4 | ||
|
|
e2f94cb1e7 | ||
|
|
acbd71666b | ||
|
|
3c35026a32 | ||
|
|
aca375d9da | ||
|
|
91285ba1c7 | ||
|
|
758b749487 | ||
|
|
a9b16cef1e | ||
|
|
c6ab392c37 | ||
|
|
4542793175 | ||
|
|
d18a0244ec | ||
|
|
b1c772d9b9 | ||
|
|
530bbcb9eb | ||
|
|
f7ebe68bd8 | ||
|
|
e2dae00af2 | ||
|
|
bbd80d4154 | ||
|
|
c180557f52 | ||
|
|
b3174b9f0b | ||
|
|
12f1ddf6a0 | ||
|
|
db3aa4ace8 | ||
|
|
f1b742dff4 | ||
|
|
444649ee8c | ||
|
|
b9ec1ccfb5 | ||
|
|
0c7c030f78 | ||
|
|
e5f1f14aad | ||
|
|
0f72ea7d93 | ||
|
|
d1906a2c83 | ||
|
|
3964af7ade | ||
|
|
5a5c5373aa | ||
|
|
ec60e1b73e | ||
|
|
a22a75d4e1 | ||
|
|
313753bb05 | ||
|
|
459fe4d8b1 | ||
|
|
489c0441a5 | ||
|
|
d91e74d02a | ||
|
|
d120d09970 | ||
|
|
84ce979a78 | ||
|
|
88b0495386 | ||
|
|
d367e8eae4 | ||
|
|
9a3cf927c0 | ||
|
|
56cdfff672 | ||
|
|
35e6dd16e7 | ||
|
|
d0af8584c1 | ||
|
|
f1afcd1962 | ||
|
|
513b0bff1c | ||
|
|
ab037b1931 | ||
|
|
06df0ecd42 | ||
|
|
b789e13a70 | ||
|
|
95a36b00c1 | ||
|
|
41c87501c4 | ||
|
|
4f3ddcc787 | ||
|
|
d10fa9983d | ||
|
|
666226731e | ||
|
|
d096f47cac | ||
|
|
a8eedb3067 | ||
|
|
ed12a54e81 | ||
|
|
eb341314ef | ||
|
|
33cd9474ab | ||
|
|
e8a43734be | ||
|
|
8a27147f5c | ||
|
|
d00d07f828 | ||
|
|
471c11d406 | ||
|
|
dd3a9254aa | ||
|
|
71a3893636 | ||
|
|
5928c3b76a | ||
|
|
1b60712b8b | ||
|
|
335a955abd | ||
|
|
6d534df8ba | ||
|
|
9022a9a45e | ||
|
|
ce4d7ae026 | ||
|
|
b863377c87 | ||
|
|
20785ee92b | ||
|
|
3afb393f58 | ||
|
|
79543187f0 | ||
|
|
3139aec463 | ||
|
|
bb917af86e | ||
|
|
17f51b9caa | ||
|
|
dd638df118 | ||
|
|
b937a05442 | ||
|
|
9c34d18cd9 | ||
|
|
02e3798124 | ||
|
|
5d9922cc33 | ||
|
|
153011e1c9 | ||
|
|
575c196cb7 | ||
|
|
47a24d19cf | ||
|
|
0eac19b71c | ||
|
|
ca73ecc786 | ||
|
|
e8733613d7 | ||
|
|
89a1df67ec | ||
|
|
3ba0a8592e | ||
|
|
57fa7a679f | ||
|
|
446e315183 | ||
|
|
c6a9b74471 | ||
|
|
018e18cb5d | ||
|
|
303ce10e73 | ||
|
|
60e0e63a92 | ||
|
|
17e5b94782 | ||
|
|
beaf012846 | ||
|
|
38c20a342c | ||
|
|
a406cfbfe2 | ||
|
|
c970eee9d3 | ||
|
|
38e3eefb33 | ||
|
|
a2e26459d9 | ||
|
|
c1b04ee743 | ||
|
|
542eb8d47e | ||
|
|
9081528f72 | ||
|
|
8be8e395b6 | ||
|
|
4b7ea80fcf | ||
|
|
7f945afbca | ||
|
|
3b241ba24f | ||
|
|
fddd74cf24 | ||
|
|
9a3138aa35 | ||
|
|
839d8b51e3 | ||
|
|
ddf0221489 | ||
|
|
64aaca66be | ||
|
|
aac26212d7 | ||
|
|
d8d99a804f | ||
|
|
f47afae1ea | ||
|
|
8d90aac203 | ||
|
|
f1bfd8b3af | ||
|
|
579fc04c1c | ||
|
|
58927dc937 | ||
|
|
018cacac59 | ||
|
|
26effb27fe | ||
|
|
5880e922ab | ||
|
|
fe007811a5 | ||
|
|
7b882c8439 | ||
|
|
6c4cb9e62f | ||
|
|
2300081d43 | ||
|
|
0f20f918be | ||
|
|
b761b8108b | ||
|
|
46e16cc0b6 | ||
|
|
237d3786a2 | ||
|
|
1884ceb7d9 | ||
|
|
893c4df06b | ||
|
|
593ed6b9ef | ||
|
|
840e9548ed | ||
|
|
dee6b7964b | ||
|
|
2fc434d475 | ||
|
|
42ebbda00d | ||
|
|
eed791acbd | ||
|
|
54b83016af | ||
|
|
1032b1f832 | ||
|
|
fce2560abe | ||
|
|
0069bb36ef | ||
|
|
1028c4193f | ||
|
|
991b11e0f4 | ||
|
|
72c01468da | ||
|
|
eecd682033 | ||
|
|
574a3127cb | ||
|
|
55027f7b1f | ||
|
|
70ffc5559d | ||
|
|
f4be134f71 | ||
|
|
7792969cae | ||
|
|
57a3532860 | ||
|
|
facf335f0e | ||
|
|
1c3817b797 | ||
|
|
74efd50f72 | ||
|
|
87e3665b01 | ||
|
|
0b94419de4 | ||
|
|
defbd347cc | ||
|
|
7efc1f2fd0 | ||
|
|
05c2da89da | ||
|
|
b83c40419c | ||
|
|
fb0097499d | ||
|
|
c2983e3bcd | ||
|
|
80b3cc52d2 | ||
|
|
fa59b188a5 | ||
|
|
e446cc3cd8 | ||
|
|
722ca6689b | ||
|
|
1276c71e4e | ||
|
|
a51bd21153 | ||
|
|
f1bbfc4901 | ||
|
|
eb8d094bf1 | ||
|
|
b1cca95ec0 | ||
|
|
27ef78cc4b | ||
|
|
4223d88fd1 | ||
|
|
ac02f6cb49 | ||
|
|
d84601e256 | ||
|
|
4152865f5e | ||
|
|
7627e6a3a0 | ||
|
|
bbcf0eb72f | ||
|
|
d51b6582e2 | ||
|
|
bbd339b88b | ||
|
|
fcaaa38085 | ||
|
|
26f27e2698 | ||
|
|
6d8d0c5fbb | ||
|
|
08c9ab6809 | ||
|
|
c150425ee5 | ||
|
|
eae78aab93 | ||
|
|
5c2666e0f4 | ||
|
|
c64b6c7e99 | ||
|
|
318f9ab2b4 | ||
|
|
5c2894a7d4 | ||
|
|
3167034b97 | ||
|
|
3bfe8c3713 | ||
|
|
0c4e026701 | ||
|
|
c246011fe2 | ||
|
|
c290e16dc5 | ||
|
|
f8852ba6c4 | ||
|
|
52ee65d9da | ||
|
|
42e04b123a | ||
|
|
1f69d1fc04 | ||
|
|
e4b5e763eb | ||
|
|
6feb607366 | ||
|
|
cc64d9883a | ||
|
|
d4a92b4dea | ||
|
|
e2849719c1 | ||
|
|
c9541d093c | ||
|
|
339d5dc1e4 | ||
|
|
2a7a03d35e | ||
|
|
ff1c3279c8 | ||
|
|
9175a44828 | ||
|
|
deeeb633cf | ||
|
|
a50bd35a54 | ||
|
|
5bf51a2e3e | ||
|
|
448b32b9c6 | ||
|
|
01428e6007 | ||
|
|
43d1147fd7 | ||
|
|
479cc47bf5 | ||
|
|
01217a3659 | ||
|
|
0e34bceb1f | ||
|
|
c4ea8eeb41 | ||
|
|
0f83fe8c64 | ||
|
|
5624611421 | ||
|
|
49d2bb4252 | ||
|
|
b54ccff7c8 | ||
|
|
b82f7ca045 | ||
|
|
2526539266 | ||
|
|
fc75036974 | ||
|
|
ee1e819812 | ||
|
|
fceea0737d | ||
|
|
f3ac556922 | ||
|
|
2e02c14455 | ||
|
|
8f580fc231 | ||
|
|
6f596b5b16 | ||
|
|
1249e28d83 | ||
|
|
85bcaa70be | ||
|
|
2a5419513c | ||
|
|
deec5c52a4 | ||
|
|
2399a5b783 | ||
|
|
efea8c6265 | ||
|
|
2be414b20c | ||
|
|
25ebb4c5a3 | ||
|
|
ac87fd3a40 | ||
|
|
c67e2e04f1 | ||
|
|
83eeb0f190 | ||
|
|
e09cf24fbf | ||
|
|
9d2b5ade67 | ||
|
|
be4e5c9449 | ||
|
|
5926197f23 | ||
|
|
2d86f81142 | ||
|
|
f37bff4a57 | ||
|
|
c700b7663b | ||
|
|
c26e548c64 | ||
|
|
658d8fb708 | ||
|
|
cd68841d1d | ||
|
|
5853968661 | ||
|
|
10d899f7a7 | ||
|
|
4e1453e702 | ||
|
|
f3cf2a4361 | ||
|
|
2890b70add | ||
|
|
1810727a2f | ||
|
|
3d566ed0fb | ||
|
|
80979bc387 | ||
|
|
4e326e3295 | ||
|
|
cff35ba863 | ||
|
|
d975fdfba8 | ||
|
|
4e3c319ee1 | ||
|
|
e5cbd23110 | ||
|
|
82f1d490f9 | ||
|
|
c3f5b492b3 | ||
|
|
09cd81b607 | ||
|
|
8a6d912374 | ||
|
|
c392f2ac74 | ||
|
|
8e6f859269 | ||
|
|
84dce9872a | ||
|
|
c2ad457e87 | ||
|
|
0116842deb | ||
|
|
3d0522a560 | ||
|
|
3db3d47e92 | ||
|
|
f341dbe72a | ||
|
|
22f388e110 | ||
|
|
115f13f686 | ||
|
|
a30739067e | ||
|
|
bd87f27c9c | ||
|
|
07c8c724e7 | ||
|
|
ea88c1cda4 | ||
|
|
c291f0fab3 | ||
|
|
88063ebf27 | ||
|
|
8485b447d8 | ||
|
|
df20c1ac25 | ||
|
|
49b038d3d2 | ||
|
|
d1efe5f203 | ||
|
|
b16a8144ff | ||
|
|
019e33d360 | ||
|
|
01093f38fa | ||
|
|
62c2f1b965 | ||
|
|
21895a221c | ||
|
|
f3d88d6b83 | ||
|
|
09327af220 | ||
|
|
0bb2cb6137 | ||
|
|
c9538137d5 | ||
|
|
df05fd04c4 | ||
|
|
dee52bf7e2 | ||
|
|
6d455e8797 | ||
|
|
a0b776ca8b | ||
|
|
3f0db21e02 | ||
|
|
6798b73a17 | ||
|
|
b98451a4b6 | ||
|
|
af2ba3f56b | ||
|
|
9c2662ae16 | ||
|
|
9ee49d4f15 | ||
|
|
911ae86379 | ||
|
|
a12e8cb67c | ||
|
|
edd95cc93f | ||
|
|
4f1f265c4d |
13
.editorconfig
Normal file
13
.editorconfig
Normal file
@@ -0,0 +1,13 @@
|
||||
root = true
|
||||
|
||||
[*]
|
||||
charset = utf-8
|
||||
indent_style = tab
|
||||
indent_size = 4
|
||||
end_of_line = lf
|
||||
insert_final_newline = true
|
||||
trim_trailing_whitespace = true
|
||||
|
||||
[*.md]
|
||||
insert_final_newline = false
|
||||
trim_trailing_whitespace = false
|
||||
50
.eslintrc.js
Normal file
50
.eslintrc.js
Normal file
@@ -0,0 +1,50 @@
|
||||
module.exports = {
|
||||
env: {
|
||||
commonjs: true,
|
||||
es2021: true,
|
||||
node: true
|
||||
},
|
||||
extends: 'eslint:recommended',
|
||||
parserOptions: {
|
||||
ecmaVersion: 12
|
||||
},
|
||||
rules: {
|
||||
// Use tabs for indentation and require 'case' in switch to be indented 1 level (default 0)
|
||||
indent: ['error', 'tab', {SwitchCase: 1}],
|
||||
// Single quotes for strings
|
||||
quotes: ['error', 'single'],
|
||||
// Always require semicolons
|
||||
semi: ['error', 'always'],
|
||||
// Don't use 'var'
|
||||
'no-var': 'error',
|
||||
// Only use quotes in object literal keys as needed
|
||||
'quote-props': ['error', 'as-needed'],
|
||||
// Don't allow trailing spaces after a line
|
||||
'no-trailing-spaces': 'error',
|
||||
// Require spaces before and after keywords (like "if")
|
||||
'keyword-spacing': 'error',
|
||||
// Don't allow unused variables, but allow unused function args (e.g. in callbacks) and global vars
|
||||
'no-unused-vars': ['error', {vars: 'local', args: 'none', varsIgnorePattern: '^_'}],
|
||||
// Require using dot notation (obj.prop instead of obj['prop']) where possible
|
||||
'dot-notation': 'error',
|
||||
// Don't use spaces before parens in anonymous or named functions
|
||||
'space-before-function-paren': ['error', {anonymous: 'never', named: 'never', asyncArrow: 'always'}]
|
||||
|
||||
// We will NOT be using eqeqeq for a few reasons:
|
||||
// 1. I would have to go through and check every single `==` to make sure that it's not depending on loose equality checks.
|
||||
// 2. I'm only using ESLint to enforce style, not actual differences in functionality. ==/=== is not merely a style choice.
|
||||
// Yes, I know that 'no-var' is actually enforcing a difference in functionality, but in practice nobody uses
|
||||
// (or even knows about) var's hoisting functionality, so at this point it's effectively a style choice.
|
||||
// 3. A lot of the time, you actually *want* loose equality checks, especially when interacting with a web server
|
||||
// (as HTTP as no concept of anything but strings). Yes, most of our interaction is JSON, but not all. And even then,
|
||||
// not all JSON actually serializes numbers as numbers.
|
||||
// 4. `==` is really nowhere near as dangerous as memes would lead you to believe, if you know what you're doing.
|
||||
// 5. If the idea behind enforcing `===` is to prevent inexperienced developers from unwittingly introducing bugs
|
||||
// via loose quality checks, in my opinion it could be just as harmful to instruct a code quality tool to
|
||||
// naively demand that all `==` become `===`. If a developer were to build code that works, but upon opening
|
||||
// a pull request they see that ESLint demands they use `===` instead, they might just click "fix" and resubmit,
|
||||
// expecting the code quality tool to know what it's doing. But it *doesn't* know what it's doing, since it's
|
||||
// just blindly alerting when it sees `==`. The change in functionality from `==` to `===` could very well
|
||||
// introduce a bug by itself.
|
||||
}
|
||||
};
|
||||
2
.github/FUNDING.yml
vendored
Normal file
2
.github/FUNDING.yml
vendored
Normal file
@@ -0,0 +1,2 @@
|
||||
github: DoctorMcKay
|
||||
custom: 'https://www.paypal.com/cgi-bin/webscr?cmd=_donations&business=N36YVAT42CZ4G&item_name=node%2dsteamcommunity¤cy_code=USD'
|
||||
31
.github/workflows/eslint.yml
vendored
Normal file
31
.github/workflows/eslint.yml
vendored
Normal file
@@ -0,0 +1,31 @@
|
||||
name: ESLint
|
||||
|
||||
on: [push, pull_request]
|
||||
|
||||
jobs:
|
||||
lint:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
# Run for external PRs, but not on our own internal PRs as they'll be run by the push to the branch.
|
||||
if: github.event_name == 'push' || github.event.pull_request.head.repo.full_name != 'DoctorMcKay/node-steamcommunity'
|
||||
|
||||
strategy:
|
||||
matrix:
|
||||
node-version: [10.x]
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v1
|
||||
with:
|
||||
fetch-depth: 1
|
||||
|
||||
- name: Use Node.js ${{ matrix.node-version }}
|
||||
uses: actions/setup-node@v1
|
||||
with:
|
||||
node-version: ${{ matrix.node-version }}
|
||||
|
||||
- name: npm install
|
||||
working-directory: .
|
||||
run: npm install --ignore-scripts
|
||||
|
||||
- name: Run ESLint
|
||||
run: npm run lint
|
||||
52
.gitignore
vendored
52
.gitignore
vendored
@@ -1,2 +1,54 @@
|
||||
node_modules/*
|
||||
test.js
|
||||
dev/
|
||||
|
||||
# Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio and WebStorm
|
||||
# Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839
|
||||
|
||||
# User-specific stuff:
|
||||
.idea/**/workspace.xml
|
||||
.idea/**/tasks.xml
|
||||
.idea/dictionaries
|
||||
.idea/httpRequests/
|
||||
|
||||
# Sensitive or high-churn files:
|
||||
.idea/**/dataSources/
|
||||
.idea/**/dataSources.ids
|
||||
.idea/**/dataSources.local.xml
|
||||
.idea/**/sqlDataSources.xml
|
||||
.idea/**/dynamic.xml
|
||||
.idea/**/uiDesigner.xml
|
||||
|
||||
# Gradle:
|
||||
.idea/**/gradle.xml
|
||||
.idea/**/libraries
|
||||
|
||||
# CMake
|
||||
cmake-build-debug/
|
||||
cmake-build-release/
|
||||
|
||||
# Mongo Explorer plugin:
|
||||
.idea/**/mongoSettings.xml
|
||||
|
||||
## File-based project format:
|
||||
*.iws
|
||||
|
||||
## Plugin-specific files:
|
||||
|
||||
# IntelliJ
|
||||
out/
|
||||
|
||||
# mpeltonen/sbt-idea plugin
|
||||
.idea_modules/
|
||||
|
||||
# JIRA plugin
|
||||
atlassian-ide-plugin.xml
|
||||
|
||||
# Cursive Clojure plugin
|
||||
.idea/replstate.xml
|
||||
|
||||
# Crashlytics plugin (for Android Studio and IntelliJ)
|
||||
com_crashlytics_export_strings.xml
|
||||
crashlytics.properties
|
||||
crashlytics-build.properties
|
||||
fabric.properties
|
||||
|
||||
1
.idea/.name
generated
Normal file
1
.idea/.name
generated
Normal file
@@ -0,0 +1 @@
|
||||
node-steamcommunity
|
||||
13
.idea/codeStyleSettings.xml
generated
Normal file
13
.idea/codeStyleSettings.xml
generated
Normal file
@@ -0,0 +1,13 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="ProjectCodeStyleSettingsManager">
|
||||
<option name="PER_PROJECT_SETTINGS">
|
||||
<value>
|
||||
<XML>
|
||||
<option name="XML_LEGACY_SETTINGS_IMPORTED" value="true" />
|
||||
</XML>
|
||||
</value>
|
||||
</option>
|
||||
<option name="PREFERRED_PROJECT_CODE_STYLE" value="Default (1)" />
|
||||
</component>
|
||||
</project>
|
||||
45
.idea/codeStyles/Project.xml
generated
Normal file
45
.idea/codeStyles/Project.xml
generated
Normal file
@@ -0,0 +1,45 @@
|
||||
<component name="ProjectCodeStyleConfiguration">
|
||||
<code_scheme name="Project" version="173">
|
||||
<option name="OTHER_INDENT_OPTIONS">
|
||||
<value>
|
||||
<option name="USE_TAB_CHARACTER" value="true" />
|
||||
<option name="SMART_TABS" value="true" />
|
||||
</value>
|
||||
</option>
|
||||
<option name="LINE_SEPARATOR" value=" " />
|
||||
<JSCodeStyleSettings version="0">
|
||||
<option name="FORCE_SEMICOLON_STYLE" value="true" />
|
||||
<option name="SPACE_BEFORE_FUNCTION_LEFT_PARENTH" value="false" />
|
||||
<option name="USE_DOUBLE_QUOTES" value="false" />
|
||||
<option name="FORCE_QUOTE_STYlE" value="true" />
|
||||
</JSCodeStyleSettings>
|
||||
<codeStyleSettings language="CSS">
|
||||
<indentOptions>
|
||||
<option name="USE_TAB_CHARACTER" value="true" />
|
||||
<option name="SMART_TABS" value="true" />
|
||||
</indentOptions>
|
||||
</codeStyleSettings>
|
||||
<codeStyleSettings language="HTML">
|
||||
<indentOptions>
|
||||
<option name="USE_TAB_CHARACTER" value="true" />
|
||||
<option name="SMART_TABS" value="true" />
|
||||
</indentOptions>
|
||||
</codeStyleSettings>
|
||||
<codeStyleSettings language="JSON">
|
||||
<indentOptions>
|
||||
<option name="INDENT_SIZE" value="4" />
|
||||
<option name="USE_TAB_CHARACTER" value="true" />
|
||||
</indentOptions>
|
||||
</codeStyleSettings>
|
||||
<codeStyleSettings language="JavaScript">
|
||||
<option name="INDENT_CASE_FROM_SWITCH" value="false" />
|
||||
<option name="ALIGN_MULTILINE_PARAMETERS" value="false" />
|
||||
<option name="ALIGN_MULTILINE_FOR" value="false" />
|
||||
<indentOptions>
|
||||
<option name="USE_TAB_CHARACTER" value="true" />
|
||||
<option name="SMART_TABS" value="true" />
|
||||
<option name="KEEP_INDENTS_ON_EMPTY_LINES" value="true" />
|
||||
</indentOptions>
|
||||
</codeStyleSettings>
|
||||
</code_scheme>
|
||||
</component>
|
||||
6
.idea/codeStyles/codeStyleConfig.xml
generated
Normal file
6
.idea/codeStyles/codeStyleConfig.xml
generated
Normal file
@@ -0,0 +1,6 @@
|
||||
<component name="ProjectCodeStyleConfiguration">
|
||||
<state>
|
||||
<option name="USE_PER_PROJECT_SETTINGS" value="true" />
|
||||
<option name="PREFERRED_PROJECT_CODE_STYLE" value="Default" />
|
||||
</state>
|
||||
</component>
|
||||
3
.idea/copyright/profiles_settings.xml
generated
Normal file
3
.idea/copyright/profiles_settings.xml
generated
Normal file
@@ -0,0 +1,3 @@
|
||||
<component name="CopyrightManager">
|
||||
<settings default="" />
|
||||
</component>
|
||||
6
.idea/encodings.xml
generated
Normal file
6
.idea/encodings.xml
generated
Normal file
@@ -0,0 +1,6 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="Encoding">
|
||||
<file url="PROJECT" charset="UTF-8" />
|
||||
</component>
|
||||
</project>
|
||||
13
.idea/inspectionProfiles/Project_Default.xml
generated
Normal file
13
.idea/inspectionProfiles/Project_Default.xml
generated
Normal file
@@ -0,0 +1,13 @@
|
||||
<component name="InspectionProjectProfileManager">
|
||||
<profile version="1.0">
|
||||
<option name="myName" value="Project Default" />
|
||||
<inspection_tool class="ES6ConvertRequireIntoImport" enabled="false" level="INFORMATION" enabled_by_default="false" />
|
||||
<inspection_tool class="ES6ConvertToForOf" enabled="false" level="INFORMATION" enabled_by_default="false" />
|
||||
<inspection_tool class="ES6ConvertVarToLetConst" enabled="false" level="WARNING" enabled_by_default="false" />
|
||||
<inspection_tool class="Eslint" enabled="true" level="WARNING" enabled_by_default="true" />
|
||||
<inspection_tool class="JSEqualityComparisonWithCoercion" enabled="false" level="WARNING" enabled_by_default="false" />
|
||||
<inspection_tool class="JSFunctionExpressionToArrowFunction" enabled="false" level="INFORMATION" enabled_by_default="false" />
|
||||
<inspection_tool class="JSStringConcatenationToES6Template" enabled="false" level="INFORMATION" enabled_by_default="false" />
|
||||
<inspection_tool class="JSUnfilteredForInLoop" enabled="false" level="WARNING" enabled_by_default="false" />
|
||||
</profile>
|
||||
</component>
|
||||
6
.idea/jsLibraryMappings.xml
generated
Normal file
6
.idea/jsLibraryMappings.xml
generated
Normal file
@@ -0,0 +1,6 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="JavaScriptLibraryMappings">
|
||||
<includedPredefinedLibrary name="Node.js Core" />
|
||||
</component>
|
||||
</project>
|
||||
6
.idea/misc.xml
generated
Normal file
6
.idea/misc.xml
generated
Normal file
@@ -0,0 +1,6 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="JavaScriptSettings">
|
||||
<option name="languageLevel" value="ES6" />
|
||||
</component>
|
||||
</project>
|
||||
9
.idea/modules.xml
generated
Normal file
9
.idea/modules.xml
generated
Normal file
@@ -0,0 +1,9 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="ProjectModuleManager">
|
||||
<modules>
|
||||
<module fileurl="file://$PROJECT_DIR$/.idea/steamcommunity.iml" filepath="$PROJECT_DIR$/.idea/steamcommunity.iml" />
|
||||
<module fileurl="file://$PROJECT_DIR$/../steamcommunity Wiki/.idea/steamcommunity Wiki.iml" filepath="$PROJECT_DIR$/../steamcommunity Wiki/.idea/steamcommunity Wiki.iml" />
|
||||
</modules>
|
||||
</component>
|
||||
</project>
|
||||
8
.idea/node-steamcommunity.iml
generated
Normal file
8
.idea/node-steamcommunity.iml
generated
Normal file
@@ -0,0 +1,8 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<module type="WEB_MODULE" version="4">
|
||||
<component name="NewModuleRootManager">
|
||||
<content url="file://$MODULE_DIR$" />
|
||||
<orderEntry type="inheritedJdk" />
|
||||
<orderEntry type="sourceFolder" forTests="false" />
|
||||
</component>
|
||||
</module>
|
||||
10
.idea/steamcommunity.iml
generated
Normal file
10
.idea/steamcommunity.iml
generated
Normal file
@@ -0,0 +1,10 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<module type="WEB_MODULE" version="4">
|
||||
<component name="NewModuleRootManager" inherit-compiler-output="true">
|
||||
<exclude-output />
|
||||
<content url="file://$MODULE_DIR$" />
|
||||
<orderEntry type="inheritedJdk" />
|
||||
<orderEntry type="sourceFolder" forTests="false" />
|
||||
<orderEntry type="module" module-name="steamcommunity Wiki" />
|
||||
</component>
|
||||
</module>
|
||||
7
.idea/vcs.xml
generated
Normal file
7
.idea/vcs.xml
generated
Normal file
@@ -0,0 +1,7 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="VcsDirectoryMappings">
|
||||
<mapping directory="" vcs="Git" />
|
||||
<mapping directory="$PROJECT_DIR$/../steamcommunity Wiki" vcs="Git" />
|
||||
</component>
|
||||
</project>
|
||||
4
.idea/watcherTasks.xml
generated
Normal file
4
.idea/watcherTasks.xml
generated
Normal file
@@ -0,0 +1,4 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="ProjectTasksOptions" suppressed-tasks="Babel" />
|
||||
</project>
|
||||
36
CONTRIBUTING.md
Normal file
36
CONTRIBUTING.md
Normal file
@@ -0,0 +1,36 @@
|
||||
# Contributing to SteamCommunity
|
||||
|
||||
Thanks for your interest in making `SteamCommunity` better! I'd appreciate it if you read over this document quickly
|
||||
before you submit your issue or pull request. It'll make things go much smoother.
|
||||
|
||||
# Issues
|
||||
|
||||
Submitting an issue?
|
||||
|
||||
- If you're **reporting a bug**, please include all relevant details.
|
||||
- A descriptive title helps for one. Titles of just "Error" or "It doesn't work" really don't help.
|
||||
- Please describe what you're trying to do, what actually happens, and what you can do to reproduce the problem.
|
||||
- If you have an error message or a crash, please include the full text of the error message and the stack trace.
|
||||
- Include the relevant snippet of your code. Wrap it in \`\`\`js /* code */ \`\`\` and GitHub [will format it nicely for you](https://help.github.com/articles/github-flavored-markdown/#syntax-highlighting).
|
||||
- If you're **requesting a feature**, please be descriptive and understanding.
|
||||
- A good title makes a difference. Please briefly describe what you're requesting in the title.
|
||||
- Be descriptive in the issue body, too. Say what you want to do, and ideally what the method should be named.
|
||||
- Be understanding if I don't think that your feature request falls within the scope of this module.
|
||||
- If you're **asking a question** or **requesting support**, please don't submit a GitHub issue.
|
||||
- Issues are only for problems directly relating to the module's code.
|
||||
- Please [post a thread in the dedicated forum](https://dev.doctormckay.com/forum/8-node-steamcommunity/) instead.
|
||||
|
||||
# Pull Requests
|
||||
|
||||
Submitting a pull request? Great! Thanks for contributing your time and code! Please keep the following in mind.
|
||||
|
||||
- Please follow the existing code style.
|
||||
- Tabs for indentation
|
||||
- camelCase for variables and functions
|
||||
- Opening braces on the same line as the if/for/while statement
|
||||
- No omitting unnecessary braces
|
||||
- etc.
|
||||
- Please avoid breaking changes. If you make a breaking change that can be done in a backwards-compatible manner, I won't accept it.
|
||||
- Please don't increment the version number in `package.json`. I'll do that myself when I publish it to npm.
|
||||
- Please include a brief description of your change in the pull request if it's not immediately apparent from the code.
|
||||
- Be understanding if I don't think that your change falls within the scope of this module.
|
||||
21
LICENSE
Normal file
21
LICENSE
Normal file
@@ -0,0 +1,21 @@
|
||||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2015 Alex Corn
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in
|
||||
all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
THE SOFTWARE.
|
||||
23
README.md
23
README.md
@@ -1,15 +1,22 @@
|
||||
# Steam Community for Node.js
|
||||
[](https://npmjs.com/package/steamcommunity)
|
||||
[](https://npmjs.com/package/steamcommunity)
|
||||
[](https://github.com/DoctorMcKay/node-steamcommunity/blob/master/LICENSE)
|
||||
[](https://www.paypal.com/cgi-bin/webscr?cmd=_donations&business=N36YVAT42CZ4G&item_name=node%2dsteamcommunity¤cy_code=USD)
|
||||
|
||||
This lightweight module provides an easy interface for the Steam Community website. This module can be used to simply login to steamcommunity.com for use with other libraries, or to interact with steamcommunity.com.
|
||||
This module provides an easy interface for the Steam Community website. This module can be used to simply login to steamcommunity.com for use with other libraries, or to interact with steamcommunity.com.
|
||||
|
||||
It supports Steam Guard but doesn't support CAPTCHAs, so make sure you don't provide Steam with an incorrect password.
|
||||
It supports Steam Guard and CAPTCHAs.
|
||||
|
||||
# Installation
|
||||
|
||||
Install it from npm:
|
||||
|
||||
$ npm install steamcommunity
|
||||
**Have a question about the module or coding in general? *Do not create a GitHub issue.* GitHub issues are for feature
|
||||
requests and bug reports. Instead, post in the [dedicated forum](https://dev.doctormckay.com/forum/8-node-steamcommunity/).
|
||||
Such issues may be ignored!**
|
||||
|
||||
# Documentation
|
||||
|
||||
As of version 1.0.0, documentation is on the [GitHub wiki](https://github.com/DoctorMcKay/node-steamcommunity/wiki).
|
||||
Documentation is available on the [GitHub wiki](https://github.com/DoctorMcKay/node-steamcommunity/wiki).
|
||||
|
||||
# Support
|
||||
|
||||
Report bugs on the [issue tracker](https://github.com/DoctorMcKay/node-steamcommunity/issues) or ask questions
|
||||
on the [dedicated forum](https://dev.doctormckay.com/forum/8-node-steamcommunity/).
|
||||
|
||||
52
classes/CConfirmation.js
Normal file
52
classes/CConfirmation.js
Normal file
@@ -0,0 +1,52 @@
|
||||
const StdLib = require('@doctormckay/stdlib');
|
||||
|
||||
const SteamCommunity = require('../index.js');
|
||||
|
||||
module.exports = CConfirmation;
|
||||
|
||||
function CConfirmation(community, data) {
|
||||
Object.defineProperty(this, '_community', {value: community});
|
||||
|
||||
this.id = data.id.toString();
|
||||
this.type = data.type;
|
||||
this.creator = data.creator.toString();
|
||||
this.key = data.key;
|
||||
this.title = data.title;
|
||||
this.receiving = data.receiving;
|
||||
this.sending = data.sending;
|
||||
this.time = data.time;
|
||||
this.timestamp = data.timestamp;
|
||||
this.icon = data.icon;
|
||||
this.offerID = this.type == SteamCommunity.ConfirmationType.Trade ? this.creator : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {number} time
|
||||
* @param {string} key
|
||||
* @param {function} [callback]
|
||||
* @return Promise<{offerID: number}>
|
||||
*/
|
||||
CConfirmation.prototype.getOfferID = function(time, key, callback) {
|
||||
return StdLib.Promises.callbackPromise(['offerID'], null, false, async (resolve, reject) => {
|
||||
if (this.type && this.creator) {
|
||||
if (this.type != SteamCommunity.ConfirmationType.Trade) {
|
||||
return reject(new Error('Not a trade confirmation'));
|
||||
}
|
||||
|
||||
return resolve({offerID: this.creator});
|
||||
}
|
||||
|
||||
return await this._community.getConfirmationOfferID(this.id, time, key, callback);
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* @param {number} time
|
||||
* @param {string} key
|
||||
* @param {boolean} accept
|
||||
* @param {function} [callback]
|
||||
* @return Promise<void>
|
||||
*/
|
||||
CConfirmation.prototype.respond = function(time, key, accept, callback) {
|
||||
return this._community.respondToConfirmation(this.id, this.key, time, key, accept, callback);
|
||||
};
|
||||
113
classes/CEconItem.js
Normal file
113
classes/CEconItem.js
Normal file
@@ -0,0 +1,113 @@
|
||||
module.exports = CEconItem;
|
||||
|
||||
function CEconItem(item, description, contextID) {
|
||||
for (let thing in item) {
|
||||
this[thing] = item[thing];
|
||||
}
|
||||
|
||||
let isCurrency = !!(this.is_currency || this.currency) || typeof this.currencyid !== 'undefined'; // I don't want to put this on the object yet; it's nice to have the ids at the top of printed output
|
||||
|
||||
if (isCurrency) {
|
||||
this.currencyid = this.id = (this.id || this.currencyid);
|
||||
} else {
|
||||
this.assetid = this.id = (this.id || this.assetid);
|
||||
}
|
||||
|
||||
this.instanceid = this.instanceid || '0';
|
||||
this.amount = parseInt(this.amount, 10);
|
||||
this.contextid = this.contextid || contextID.toString();
|
||||
|
||||
// Merge the description
|
||||
if (description) {
|
||||
// Is this a listing of descriptions?
|
||||
if (description[this.classid + '_' + this.instanceid]) {
|
||||
description = description[this.classid + '_' + this.instanceid];
|
||||
}
|
||||
|
||||
for (let thing in description) {
|
||||
this[thing] = description[thing];
|
||||
}
|
||||
}
|
||||
|
||||
this.is_currency = isCurrency;
|
||||
this.tradable = !!this.tradable;
|
||||
this.marketable = !!this.marketable;
|
||||
this.commodity = !!this.commodity;
|
||||
this.market_tradable_restriction = (this.market_tradable_restriction ? parseInt(this.market_tradable_restriction, 10) : 0);
|
||||
this.market_marketable_restriction = (this.market_marketable_restriction ? parseInt(this.market_marketable_restriction, 10) : 0);
|
||||
this.fraudwarnings = this.fraudwarnings || [];
|
||||
this.descriptions = this.descriptions || [];
|
||||
|
||||
if (this.owner && JSON.stringify(this.owner) == '{}') {
|
||||
this.owner = null;
|
||||
}
|
||||
|
||||
// Restore old property names of tags
|
||||
if (this.tags) {
|
||||
this.tags = this.tags.map((tag) => ({
|
||||
internal_name: tag.internal_name,
|
||||
name: tag.localized_tag_name || tag.name,
|
||||
category: tag.category,
|
||||
color: tag.color || '',
|
||||
category_name: tag.localized_category_name || tag.category_name
|
||||
}));
|
||||
}
|
||||
|
||||
// Restore market_fee_app, if applicable
|
||||
let match;
|
||||
if (this.appid == 753 && this.contextid == 6 && this.market_hash_name && (match = this.market_hash_name.match(/^(\d+)-/))) {
|
||||
this.market_fee_app = parseInt(match[1], 10);
|
||||
}
|
||||
|
||||
// Restore cache_expiration, if we can (for CS:GO items)
|
||||
if (this.appid == 730 && this.contextid == 2 && this.owner_descriptions) {
|
||||
let description = this.owner_descriptions.find(d => d.value && d.value.indexOf('Tradable After ') == 0);
|
||||
if (description) {
|
||||
let date = new Date(description.value.substring(15).replace(/[,()]/g, ''));
|
||||
if (date) {
|
||||
this.cache_expiration = date.toISOString();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// If we have item_expiration, also set cache_expiration to the same value
|
||||
if (this.item_expiration) {
|
||||
this.cache_expiration = this.item_expiration;
|
||||
}
|
||||
|
||||
if (this.actions === '') {
|
||||
this.actions = [];
|
||||
}
|
||||
|
||||
// One wouldn't think that we need this if statement, but apparently v8 has a weird bug/quirk where deleting a
|
||||
// property results in greatly increased memory usage. Because that makes sense.
|
||||
if (this.currency) {
|
||||
delete this.currency;
|
||||
}
|
||||
}
|
||||
|
||||
CEconItem.prototype.getImageURL = function() {
|
||||
return 'https://steamcommunity-a.akamaihd.net/economy/image/' + this.icon_url + '/';
|
||||
};
|
||||
|
||||
CEconItem.prototype.getLargeImageURL = function() {
|
||||
if (!this.icon_url_large) {
|
||||
return this.getImageURL();
|
||||
}
|
||||
|
||||
return 'https://steamcommunity-a.akamaihd.net/economy/image/' + this.icon_url_large + '/';
|
||||
};
|
||||
|
||||
CEconItem.prototype.getTag = function(category) {
|
||||
if (!this.tags) {
|
||||
return null;
|
||||
}
|
||||
|
||||
for (let i = 0; i < this.tags.length; i++) {
|
||||
if (this.tags[i].category == category) {
|
||||
return this.tags[i];
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
};
|
||||
@@ -1,126 +1,187 @@
|
||||
var SteamCommunity = require('../index.js');
|
||||
var Cheerio = require('cheerio');
|
||||
const Cheerio = require('cheerio');
|
||||
|
||||
SteamCommunity.prototype.getMarketItem = function(appid, hashName, callback) {
|
||||
var self = this;
|
||||
this.request("https://steamcommunity.com/market/listings/" + appid + "/" + encodeURIComponent(hashName), function(err, response, body) {
|
||||
if(self._checkHttpError(err, response, callback)) {
|
||||
const SteamCommunity = require('../index.js');
|
||||
|
||||
SteamCommunity.prototype.getMarketItem = function(appid, hashName, currency, callback) {
|
||||
if (typeof currency == 'function') {
|
||||
callback = currency;
|
||||
currency = 1;
|
||||
}
|
||||
|
||||
this.httpRequest('https://steamcommunity.com/market/listings/' + appid + '/' + encodeURIComponent(hashName), (err, response, body) => {
|
||||
if (err) {
|
||||
callback(err);
|
||||
return;
|
||||
}
|
||||
|
||||
var $ = Cheerio.load(body);
|
||||
if($('.market_listing_table_message') && $('.market_listing_table_message').text().trim() == 'There are no listings for this item.') {
|
||||
callback(new Error("There are no listings for this item."));
|
||||
|
||||
let $ = Cheerio.load(body);
|
||||
let $listingTableMessage = $('.market_listing_table_message');
|
||||
if ($listingTableMessage && $listingTableMessage.text().trim() == 'There are no listings for this item.') {
|
||||
callback(new Error('There are no listings for this item.'));
|
||||
return;
|
||||
}
|
||||
|
||||
var item = new CMarketItem(self, body, $);
|
||||
if(item.commodity) {
|
||||
item.updatePrice(function(err) {
|
||||
if(err) {
|
||||
callback(err);
|
||||
} else {
|
||||
callback(null, item);
|
||||
}
|
||||
});
|
||||
} else {
|
||||
callback(null, item);
|
||||
}
|
||||
});
|
||||
|
||||
let item = new CMarketItem(appid, hashName, this, body, $);
|
||||
item.updatePrice(currency, (err) => {
|
||||
if (err) {
|
||||
callback(err);
|
||||
} else {
|
||||
callback(null, item);
|
||||
}
|
||||
});
|
||||
}, 'steamcommunity');
|
||||
};
|
||||
|
||||
function CMarketItem(community, body, $) {
|
||||
function CMarketItem(appid, hashName, community, body, $) {
|
||||
this._appid = appid;
|
||||
this._hashName = hashName;
|
||||
this._community = community;
|
||||
this._$ = $;
|
||||
|
||||
this._country = "US";
|
||||
var match = body.match(/var g_strCountryCode = "([^"]+)";/);
|
||||
if(match) {
|
||||
|
||||
this._country = 'US';
|
||||
let match = body.match(/var g_strCountryCode = "([^"]+)";/);
|
||||
if (match) {
|
||||
this._country = match[1];
|
||||
}
|
||||
|
||||
|
||||
this._language = 'english';
|
||||
match = body.match(/var g_strLanguage = "([^"]+)";/);
|
||||
if (match) {
|
||||
this._language = match[1];
|
||||
}
|
||||
|
||||
this.commodity = false;
|
||||
var match = body.match(/Market_LoadOrderSpread\(\s*(\d+)\s*\);/);
|
||||
if(match) {
|
||||
match = body.match(/Market_LoadOrderSpread\(\s*(\d+)\s*\);/);
|
||||
if (match) {
|
||||
this.commodity = true;
|
||||
this.commodityID = parseInt(match[1], 10);
|
||||
}
|
||||
|
||||
|
||||
this.medianSalePrices = null;
|
||||
match = body.match(/var line1=([^;]+);/);
|
||||
if(match) {
|
||||
if (match) {
|
||||
try {
|
||||
this.medianSalePrices = JSON.parse(match[1]);
|
||||
this.medianSalePrices = this.medianSalePrices.map(function(item) {
|
||||
return {
|
||||
"hour": new Date(item[0]),
|
||||
"price": item[1],
|
||||
"quantity": parseInt(item[2], 10)
|
||||
};
|
||||
});
|
||||
} catch(e) {
|
||||
this.medianSalePrices = this.medianSalePrices.map((item) => ({
|
||||
hour: new Date(item[0]),
|
||||
price: item[1],
|
||||
quantity: parseInt(item[2], 10)
|
||||
}));
|
||||
} catch (e) {
|
||||
// ignore
|
||||
}
|
||||
}
|
||||
|
||||
this.quantity = 0;
|
||||
this.lowestPrice = 0;
|
||||
|
||||
if(!this.commodity) {
|
||||
var total = $('#searchResults_total');
|
||||
if(total) {
|
||||
this.quantity = parseInt(total.text().replace(/[^\d]/g, '').trim(), 10);
|
||||
}
|
||||
|
||||
var lowest = $('.market_listing_price.market_listing_price_with_fee');
|
||||
if(lowest[0]) {
|
||||
this.lowestPrice = parseInt($(lowest[0]).text().replace(/[^\d]/g, '').trim(), 10);
|
||||
|
||||
this.firstAsset = null;
|
||||
this.assets = null;
|
||||
match = body.match(/var g_rgAssets = (.*);/);
|
||||
if (match) {
|
||||
try {
|
||||
this.assets = JSON.parse(match[1]);
|
||||
this.assets = this.assets[appid];
|
||||
this.assets = this.assets[Object.keys(this.assets)[0]];
|
||||
this.firstAsset = this.assets[Object.keys(this.assets)[0]];
|
||||
} catch (e) {
|
||||
// ignore
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
this.quantity = 0;
|
||||
this.lowestPrice = 0;
|
||||
// TODO: Buying listings and placing buy orders
|
||||
}
|
||||
|
||||
CMarketItem.prototype.updatePrice = function(callback) {
|
||||
if(!this.commodity) {
|
||||
throw new Error("Cannot update price for non-commodity item");
|
||||
CMarketItem.prototype.updatePrice = function(currency, callback) {
|
||||
if (this.commodity) {
|
||||
this.updatePriceForCommodity(currency, callback);
|
||||
} else {
|
||||
this.updatePriceForNonCommodity(currency, callback);
|
||||
}
|
||||
|
||||
// TODO: Currency option maybe?
|
||||
var self = this;
|
||||
this._community.request({
|
||||
"uri": "https://steamcommunity.com/market/itemordershistogram?country=US&language=english¤cy=1&item_nameid=" + this.commodityID,
|
||||
"json": true,
|
||||
}, function(err, response, body) {
|
||||
if(self._checkHttpError(err, response, callback)) {
|
||||
};
|
||||
|
||||
CMarketItem.prototype.updatePriceForCommodity = function(currency, callback) {
|
||||
if (!this.commodity) {
|
||||
throw new Error('Cannot update price for non-commodity item');
|
||||
}
|
||||
|
||||
this._community.httpRequest({
|
||||
url: 'https://steamcommunity.com/market/itemordershistogram?country=US&language=english¤cy=' + currency + '&item_nameid=' + this.commodityID,
|
||||
json: true
|
||||
}, (err, response, body) => {
|
||||
if (err) {
|
||||
callback(err);
|
||||
return;
|
||||
}
|
||||
|
||||
if(body.success != 1) {
|
||||
if(callback) {
|
||||
callback(new Error("Error " + body.success));
|
||||
|
||||
if (body.success != 1) {
|
||||
if (callback) {
|
||||
callback(new Error('Error ' + body.success));
|
||||
}
|
||||
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
var match = (body.sell_order_summary || '').match(/<span class="market_commodity_orders_header_promote">(\d+)<\/span>/);
|
||||
if(match) {
|
||||
self.quantity = parseInt(match[1], 10);
|
||||
|
||||
let match = (body.sell_order_summary || '').match(/<span class="market_commodity_orders_header_promote">(\d+)<\/span>/);
|
||||
if (match) {
|
||||
this.quantity = parseInt(match[1], 10);
|
||||
}
|
||||
|
||||
self.buyQuantity = 0;
|
||||
|
||||
this.buyQuantity = 0;
|
||||
match = (body.buy_order_summary || '').match(/<span class="market_commodity_orders_header_promote">(\d+)<\/span>/);
|
||||
if(match) {
|
||||
self.buyQuantity = parseInt(match[1], 10);
|
||||
if (match) {
|
||||
this.buyQuantity = parseInt(match[1], 10);
|
||||
}
|
||||
|
||||
self.lowestPrice = parseInt(body.lowest_sell_order, 10);
|
||||
self.highestBuyOrder = parseInt(body.highest_buy_order, 10);
|
||||
|
||||
|
||||
this.lowestPrice = parseInt(body.lowest_sell_order, 10);
|
||||
this.highestBuyOrder = parseInt(body.highest_buy_order, 10);
|
||||
|
||||
// TODO: The tables?
|
||||
if(callback) {
|
||||
if (callback) {
|
||||
callback(null);
|
||||
}
|
||||
});
|
||||
}, 'steamcommunity');
|
||||
};
|
||||
|
||||
CMarketItem.prototype.updatePriceForNonCommodity = function(currency, callback) {
|
||||
if (this.commodity) {
|
||||
throw new Error('Cannot update price for commodity item');
|
||||
}
|
||||
|
||||
this._community.httpRequest({
|
||||
url: 'https://steamcommunity.com/market/listings/' +
|
||||
this._appid + '/' +
|
||||
encodeURIComponent(this._hashName) +
|
||||
'/render/?query=&start=0&count=10&country=US&language=english¤cy=' + currency,
|
||||
json: true
|
||||
}, (err, response, body) => {
|
||||
if (err) {
|
||||
callback(err);
|
||||
return;
|
||||
}
|
||||
|
||||
if (body.success != 1) {
|
||||
callback && callback(new Error('Error ' + body.success));
|
||||
return;
|
||||
}
|
||||
|
||||
let match = body.total_count;
|
||||
if (match) {
|
||||
this.quantity = parseInt(match, 10);
|
||||
}
|
||||
|
||||
let lowestPrice;
|
||||
let $ = Cheerio.load(body.results_html);
|
||||
match = $('.market_listing_price.market_listing_price_with_fee');
|
||||
if (match) {
|
||||
for (let i = 0; i < match.length; i++) {
|
||||
lowestPrice = parseFloat($(match[i]).text().replace(',', '.').replace(/[^\d.]/g, ''));
|
||||
if (!isNaN(lowestPrice)) {
|
||||
this.lowestPrice = lowestPrice;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
callback && callback(null);
|
||||
}, 'steamcommunity');
|
||||
};
|
||||
|
||||
@@ -1,84 +1,89 @@
|
||||
var SteamCommunity = require('../index.js');
|
||||
var Cheerio = require('cheerio');
|
||||
const Cheerio = require('cheerio');
|
||||
|
||||
const SteamCommunity = require('../index.js');
|
||||
|
||||
SteamCommunity.prototype.marketSearch = function(options, callback) {
|
||||
var qs = {};
|
||||
|
||||
if(typeof options === 'string') {
|
||||
let qs = {};
|
||||
|
||||
if (typeof options === 'string') {
|
||||
qs.query = options;
|
||||
} else {
|
||||
qs.query = options.query || '';
|
||||
qs.appid = options.appid;
|
||||
qs.search_descriptions = options.searchDescriptions ? 1 : 0;
|
||||
|
||||
if(qs.appid) {
|
||||
for(var i in options) {
|
||||
if(['query', 'appid', 'searchDescriptions'].indexOf(i) != -1) {
|
||||
|
||||
if (qs.appid) {
|
||||
for (let i in options) {
|
||||
if (['query', 'appid', 'searchDescriptions'].indexOf(i) != -1) {
|
||||
continue;
|
||||
}
|
||||
|
||||
|
||||
// This is a tag
|
||||
qs['category_' + qs.appid + '_' + i + '[]'] = 'tag_' + options[i];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
qs.start = 0;
|
||||
qs.count = 100;
|
||||
qs.sort_column = 'price';
|
||||
qs.sort_dir = 'asc';
|
||||
performSearch(this.request, qs, [], callback);
|
||||
|
||||
let results = [];
|
||||
const performSearch = () => {
|
||||
this.httpRequest({
|
||||
url: 'https://steamcommunity.com/market/search/render/',
|
||||
qs: qs,
|
||||
headers: {
|
||||
referer: 'https://steamcommunity.com/market/search'
|
||||
},
|
||||
json: true
|
||||
}, (err, response, body) => {
|
||||
if (err) {
|
||||
callback(err);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!body.success) {
|
||||
callback(new Error('Success is not true'));
|
||||
return;
|
||||
}
|
||||
|
||||
if (!body.results_html) {
|
||||
callback(new Error('No results_html in response'));
|
||||
return;
|
||||
}
|
||||
|
||||
let $ = Cheerio.load(body.results_html);
|
||||
let $errorMsg = $('.market_listing_table_message');
|
||||
if ($errorMsg.length > 0) {
|
||||
callback(new Error($errorMsg.text()));
|
||||
return;
|
||||
}
|
||||
|
||||
let rows = $('.market_listing_row_link');
|
||||
for (let i = 0; i < rows.length; i++) {
|
||||
results.push(new CMarketSearchResult($(rows[i])));
|
||||
}
|
||||
|
||||
if (body.start + body.pagesize >= body.total_count) {
|
||||
callback(null, results);
|
||||
} else {
|
||||
qs.start += body.pagesize;
|
||||
performSearch();
|
||||
}
|
||||
}, 'steamcommunity');
|
||||
};
|
||||
|
||||
performSearch();
|
||||
};
|
||||
|
||||
function performSearch(request, qs, results, callback) {
|
||||
request({
|
||||
"uri": "https://steamcommunity.com/market/search/render/",
|
||||
"qs": qs,
|
||||
"headers": {
|
||||
"referer": "https://steamcommunity.com/market/search"
|
||||
},
|
||||
"json": true
|
||||
}, function(err, response, body) {
|
||||
if(self._checkHttpError(err, response, callback)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if(!body.success) {
|
||||
callback(new Error("Success is not true"));
|
||||
return;
|
||||
}
|
||||
|
||||
if(!body.results_html) {
|
||||
callback(new Error("No results_html in response"));
|
||||
return;
|
||||
}
|
||||
|
||||
var $ = Cheerio.load(body.results_html);
|
||||
if($('.market_listing_table_message').length > 0) {
|
||||
callback(new Error($('.market_listing_table_message').text()));
|
||||
return;
|
||||
}
|
||||
|
||||
var rows = $('.market_listing_row_link');
|
||||
for(var i = 0; i < rows.length; i++) {
|
||||
results.push(new CMarketSearchResult($(rows[i])));
|
||||
}
|
||||
|
||||
if(body.start + body.pagesize >= body.total_count) {
|
||||
callback(null, results);
|
||||
} else {
|
||||
qs.start += body.pagesize;
|
||||
performSearch(request, qs, results, callback);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function CMarketSearchResult(row) {
|
||||
var match = row.attr('href').match(/\/market\/listings\/(\d+)\/(.+)/);
|
||||
|
||||
let match = row.attr('href').match(/\/market\/listings\/(\d+)\/([^?/]+)/);
|
||||
|
||||
this.appid = parseInt(match[1], 10);
|
||||
this.market_hash_name = decodeURIComponent(match[2]);
|
||||
this.image = row.find('.market_listing_item_img').attr('src').match(/^https?:\/\/[^\/]+\/economy\/image\/[^\/]+\//)[0];
|
||||
this.price = parseInt(row.find('.market_listing_their_price .market_table_value span').text().replace(/[^\d]+/g, ''), 10);
|
||||
this.image = ((row.find('.market_listing_item_img').attr('src') || '').match(/^https?:\/\/[^/]+\/economy\/image\/[^/]+\//) || [])[0];
|
||||
this.price = parseInt(row.find('.market_listing_their_price .market_table_value span.normal_price').text().replace(/[^\d]+/g, ''), 10);
|
||||
this.quantity = parseInt(row.find('.market_listing_num_listings_qty').text().replace(/[^\d]+/g, ''), 10);
|
||||
}
|
||||
|
||||
@@ -1,40 +1,38 @@
|
||||
var SteamCommunity = require('../index.js');
|
||||
var SteamID = require('steamid');
|
||||
var xml2js = require('xml2js');
|
||||
const SteamID = require('steamid');
|
||||
const XML2JS = require('xml2js');
|
||||
|
||||
const Helpers = require('../components/helpers.js');
|
||||
const SteamCommunity = require('../index.js');
|
||||
|
||||
SteamCommunity.prototype.getSteamGroup = function(id, callback) {
|
||||
if(typeof id !== 'string' && !(typeof id === 'object' && id.__proto__ === SteamID.prototype)) {
|
||||
throw new Error("id parameter should be a group URL string or a SteamID object");
|
||||
if (typeof id !== 'string' && !Helpers.isSteamID(id)) {
|
||||
throw new Error('id parameter should be a group URL string or a SteamID object');
|
||||
}
|
||||
|
||||
if(typeof id === 'object' && (id.universe != SteamID.Universe.PUBLIC || id.type != SteamID.Type.CLAN)) {
|
||||
throw new Error("SteamID must stand for a clan account in the public universe");
|
||||
|
||||
if (typeof id === 'object' && (id.universe != SteamID.Universe.PUBLIC || id.type != SteamID.Type.CLAN)) {
|
||||
throw new Error('SteamID must stand for a clan account in the public universe');
|
||||
}
|
||||
|
||||
var self = this;
|
||||
this.request("https://steamcommunity.com/" + (typeof id === 'string' ? "groups/" + id : "gid/" + id.toString()) + "/memberslistxml/?xml=1", function(err, response, body) {
|
||||
if(self._checkHttpError(err, response, callback)) {
|
||||
|
||||
this.httpRequest('https://steamcommunity.com/' + (typeof id === 'string' ? 'groups/' + id : 'gid/' + id.toString()) + '/memberslistxml/?xml=1', (err, response, body) => {
|
||||
if (err) {
|
||||
callback(err);
|
||||
return;
|
||||
}
|
||||
|
||||
if(self._checkCommunityError(body, callback)) {
|
||||
return;
|
||||
}
|
||||
|
||||
xml2js.parseString(body, function(err, result) {
|
||||
if(err) {
|
||||
|
||||
XML2JS.parseString(body, (err, result) => {
|
||||
if (err) {
|
||||
callback(err);
|
||||
return;
|
||||
}
|
||||
|
||||
callback(null, new CSteamGroup(self, result.memberList));
|
||||
|
||||
callback(null, new CSteamGroup(this, result.memberList));
|
||||
});
|
||||
});
|
||||
}, 'steamcommunity');
|
||||
};
|
||||
|
||||
function CSteamGroup(community, groupData) {
|
||||
this._community = community;
|
||||
|
||||
|
||||
this.steamID = new SteamID(groupData.groupID64[0]);
|
||||
this.name = groupData.groupDetails[0].groupName[0];
|
||||
this.url = groupData.groupDetails[0].groupURL[0];
|
||||
@@ -50,237 +48,108 @@ function CSteamGroup(community, groupData) {
|
||||
CSteamGroup.prototype.getAvatarURL = function(size, protocol) {
|
||||
size = size || '';
|
||||
protocol = protocol || 'http://';
|
||||
|
||||
var url = protocol + "steamcdn-a.akamaihd.net/steamcommunity/public/images/avatars/" + this.avatarHash.substring(0, 2) + "/" + this.avatarHash;
|
||||
if(size == 'full' || size == 'medium') {
|
||||
return url + "_" + size + ".jpg";
|
||||
|
||||
let url = protocol + 'steamcdn-a.akamaihd.net/steamcommunity/public/images/avatars/' + this.avatarHash.substring(0, 2) + '/' + this.avatarHash;
|
||||
if (size == 'full' || size == 'medium') {
|
||||
return url + '_' + size + '.jpg';
|
||||
} else {
|
||||
return url + ".jpg";
|
||||
return url + '.jpg';
|
||||
}
|
||||
};
|
||||
|
||||
CSteamGroup.prototype.getMembers = function(callback, members, link) {
|
||||
members = members || [];
|
||||
link = link || "http://steamcommunity.com/gid/" + this.steamID.toString() + "/memberslistxml/?xml=1";
|
||||
|
||||
var self = this;
|
||||
this._community.request(link, function(err, response, body) {
|
||||
if(self._checkHttpError(err, response, callback)) {
|
||||
return;
|
||||
}
|
||||
|
||||
xml2js.parseString(body, function(err, result) {
|
||||
if(err) {
|
||||
callback(err);
|
||||
return;
|
||||
}
|
||||
|
||||
members = members.concat(result.memberList.members[0].steamID64.map(function(steamID) {
|
||||
return new SteamID(steamID);
|
||||
}));
|
||||
|
||||
if(result.memberList.nextPageLink) {
|
||||
self.getMembers(callback, members, result.memberList.nextPageLink[0]);
|
||||
} else {
|
||||
callback(null, members);
|
||||
}
|
||||
});
|
||||
});
|
||||
CSteamGroup.prototype.getMembers = function(addresses, callback) {
|
||||
if (typeof addresses === 'function') {
|
||||
callback = addresses;
|
||||
addresses = null;
|
||||
}
|
||||
|
||||
this._community.getGroupMembers(this.steamID, callback, null, null, addresses, 0);
|
||||
};
|
||||
|
||||
CSteamGroup.prototype.join = function(callback) {
|
||||
var form = {
|
||||
"action": "join",
|
||||
"sessionID": this._community.getSessionID()
|
||||
};
|
||||
|
||||
var self = this;
|
||||
this._community.request.post("https://steamcommunity.com/gid/" + this.steamID.toString(), {"form": form}, function(err, response, body) {
|
||||
if(!callback) {
|
||||
return;
|
||||
}
|
||||
|
||||
if(err || response.statusCode >= 400) {
|
||||
callback(err || new Error("HTTP error " + response.statusCode));
|
||||
return;
|
||||
}
|
||||
|
||||
if(self._community._checkCommunityError(body, callback)) {
|
||||
return;
|
||||
}
|
||||
|
||||
callback(null);
|
||||
});
|
||||
this._community.joinGroup(this.steamID, callback);
|
||||
};
|
||||
|
||||
CSteamGroup.prototype.leave = function(callback) {
|
||||
var form = {
|
||||
"sessionID": this._community.getSessionID(),
|
||||
"action": "leaveGroup",
|
||||
"groupId": this.steamID.toString()
|
||||
};
|
||||
|
||||
var self = this;
|
||||
this._community._myProfile("home_process", form, function(err, response, body) {
|
||||
if(!callback) {
|
||||
return;
|
||||
}
|
||||
|
||||
if(err || response.statusCode >= 400) {
|
||||
callback(err || new Error("HTTP error " + response.statusCode));
|
||||
return;
|
||||
}
|
||||
|
||||
if(self._community._checkCommunityError(body, callback)) {
|
||||
return;
|
||||
}
|
||||
|
||||
callback(null);
|
||||
});
|
||||
this._community.leaveGroup(this.steamID, callback);
|
||||
};
|
||||
|
||||
CSteamGroup.prototype.postAnnouncement = function(headline, content, callback) {
|
||||
var form = {
|
||||
"sessionID": this._community.getSessionID(),
|
||||
"action": "post",
|
||||
"headline": headline,
|
||||
"body": content
|
||||
};
|
||||
|
||||
this._community.request.post("https://steamcommunity.com/gid/" + this.steamID.toString() + "/announcements", {"form": form}, function(err, response, body) {
|
||||
if(!callback) {
|
||||
return;
|
||||
}
|
||||
|
||||
if(err || response.statusCode >= 400) {
|
||||
callback(err || new Error("HTTP error " + response.statusCode));
|
||||
return;
|
||||
}
|
||||
|
||||
if(self._community._checkCommunityError(body, callback)) {
|
||||
return;
|
||||
}
|
||||
|
||||
callback(null);
|
||||
});
|
||||
CSteamGroup.prototype.getAllAnnouncements = function(time, callback) {
|
||||
this._community.getAllGroupAnnouncements(this.steamID, time, callback);
|
||||
};
|
||||
|
||||
CSteamGroup.prototype.postAnnouncement = function(headline, content, hidden, callback) {
|
||||
this._community.postGroupAnnouncement(this.steamID, headline, content, hidden, callback);
|
||||
};
|
||||
|
||||
CSteamGroup.prototype.editAnnouncement = function(annoucementID, headline, content, callback) {
|
||||
this._community.editGroupAnnouncement(this.steamID, annoucementID, headline, content, callback);
|
||||
};
|
||||
|
||||
CSteamGroup.prototype.deleteAnnouncement = function(annoucementID, callback) {
|
||||
this._community.deleteGroupAnnouncement(this.steamID, annoucementID, callback);
|
||||
};
|
||||
|
||||
CSteamGroup.prototype.scheduleEvent = function(name, type, description, time, server, callback) {
|
||||
// Event types: ChatEvent - Chat, OtherEvent - A lil somethin somethin, PartyEvent - Party!, MeetingEvent - Important meeting, SpecialCauseEvent - Special cause (charity ball?), MusicAndArtsEvent - Music or Art type thing, SportsEvent - Sporting endeavor, TripEvent - Out of town excursion
|
||||
// Passing a number for type will make it a game event for that appid
|
||||
|
||||
if(typeof server === 'function') {
|
||||
callback = server;
|
||||
server = {"ip": "", "password": ""};
|
||||
} else if(typeof server === 'string') {
|
||||
server = {"ip": server, "password": ""};
|
||||
} else {
|
||||
server = {"ip": "", "password": ""};
|
||||
}
|
||||
|
||||
var form = {
|
||||
"sessionid": this._community.getSessionID(),
|
||||
"action": "newEvent",
|
||||
"tzOffset": new Date().getTimezoneOffset() * -60,
|
||||
"name": name,
|
||||
"type": (typeof type === 'number' || !isNaN(parseInt(type, 10)) ? "GameEvent" : type),
|
||||
"appID": (typeof type === 'number' || !isNaN(parseInt(type, 10)) ? type : ''),
|
||||
"serverIP": server.ip,
|
||||
"serverPassword": server.password,
|
||||
"notes": description,
|
||||
"eventQuickTime": "now"
|
||||
};
|
||||
|
||||
if(time === null) {
|
||||
form.startDate = 'MM/DD/YY';
|
||||
form.startHour = '12';
|
||||
form.startMinute = '00';
|
||||
form.startAMPM = 'PM';
|
||||
form.timeChoice = 'quick';
|
||||
} else {
|
||||
form.startDate = (time.getMonth() + 1 < 10 ? '0' : '') + (time.getMonth() + 1) + '/' + (time.getDate() < 10 ? '0' : '') + time.getDate() + '/' + time.getFullYear().toString().substring(2);
|
||||
form.startHour = (time.getHours() === 0 ? '12' : (time.getHours() > 12 ? time.getHours() - 12 : time.getHours()));
|
||||
form.startMinute = (time.getMinutes() < 10 ? '0' : '') + time.getMinutes();
|
||||
form.startAMPM = (time.getHours() <= 12 ? 'AM' : 'PM');
|
||||
form.timeChoice = 'specific';
|
||||
}
|
||||
|
||||
var self = this;
|
||||
this._community.request.post("https://steamcommunity.com/gid/" + this.steamID.toString() + "/eventEdit", {"form": form}, function(err, response, body) {
|
||||
if(!callback) {
|
||||
return;
|
||||
}
|
||||
|
||||
if(err || response.statusCode >= 400) {
|
||||
callback(err || new Error("HTTP error " + response.statusCode));
|
||||
return;
|
||||
}
|
||||
|
||||
if(self._community._checkCommunityError(body, callback)) {
|
||||
return;
|
||||
}
|
||||
|
||||
callback(null);
|
||||
});
|
||||
this._community.scheduleGroupEvent(this.steamID, name, type, description, time, server, callback);
|
||||
};
|
||||
|
||||
CSteamGroup.prototype.editEvent = function(id, name, type, description, time, server, callback) {
|
||||
this._community.editGroupEvent(this.steamID, id, name, type, description, time, server, callback);
|
||||
};
|
||||
|
||||
CSteamGroup.prototype.deleteEvent = function(id, callback) {
|
||||
this._community.deleteGroupEvent(this.steamID, id, callback);
|
||||
};
|
||||
|
||||
CSteamGroup.prototype.setPlayerOfTheWeek = function(steamID, callback) {
|
||||
var form = {
|
||||
"xml": 1,
|
||||
"action": "potw",
|
||||
"memberId": steamID.getSteam3RenderedID(),
|
||||
"sessionid": this._community.getSessionID()
|
||||
};
|
||||
|
||||
var self = this;
|
||||
this._community.request.post("https://steamcommunity.com/gid/" + this.steamID.toString() + "/potwEdit", {"form": form}, function(err, response, body) {
|
||||
if(!callback) {
|
||||
return;
|
||||
}
|
||||
|
||||
if(err || response.statusCode != 200) {
|
||||
callback(err || new Error("HTTP error " + response.statusCode));
|
||||
return;
|
||||
}
|
||||
|
||||
xml2js.parseString(body, function(err, results) {
|
||||
if(err) {
|
||||
callback(err);
|
||||
return;
|
||||
}
|
||||
|
||||
if(results.response.results[0] == 'OK') {
|
||||
callback(null, new SteamID(results.response.oldPOTW[0]), new SteamID(results.response.newPOTW[0]));
|
||||
} else {
|
||||
callback(new Error(results.response.results[0]));
|
||||
}
|
||||
});
|
||||
});
|
||||
this._community.setGroupPlayerOfTheWeek(this.steamID, steamID, callback);
|
||||
};
|
||||
|
||||
CSteamGroup.prototype.kick = function(steamID, callback) {
|
||||
var form = {
|
||||
"sessionID": this._community.getSessionID(),
|
||||
"action": "kick",
|
||||
"memberId": steamID.toString(),
|
||||
"queryString": ""
|
||||
};
|
||||
|
||||
var self = this;
|
||||
this._community.request.post("https://steamcommunity.com/gid/" + this.steamID.toString() + "/membersManage", {"form": form}, function(err, response, body) {
|
||||
if(!callback) {
|
||||
return;
|
||||
}
|
||||
|
||||
if(err || response.statusCode >= 400) {
|
||||
callback(err || new Error("HTTP error " + response.statusCode));
|
||||
return;
|
||||
}
|
||||
|
||||
if(self._community._checkCommunityError(body, callback)) {
|
||||
return;
|
||||
}
|
||||
|
||||
callback(null);
|
||||
});
|
||||
this._community.kickGroupMember(this.steamID, steamID, callback);
|
||||
};
|
||||
|
||||
CSteamGroup.prototype.getHistory = function(page, callback) {
|
||||
this._community.getGroupHistory(this.steamID, page, callback);
|
||||
};
|
||||
|
||||
|
||||
CSteamGroup.prototype.getAllComments = function(from, count, callback) {
|
||||
this._community.getAllGroupComments(this.steamID, from, count, callback);
|
||||
};
|
||||
|
||||
CSteamGroup.prototype.deleteComment = function(cid, callback) {
|
||||
this._community.deleteGroupComment(this.steamID, cid, callback);
|
||||
};
|
||||
|
||||
CSteamGroup.prototype.comment = function(message, callback) {
|
||||
this._community.postGroupComment(this.steamID, message, callback);
|
||||
};
|
||||
|
||||
/**
|
||||
* Get requests to join this restricted group.
|
||||
* @param {function} callback - First argument is null/Error, second is array of SteamID objects
|
||||
*/
|
||||
CSteamGroup.prototype.getJoinRequests = function(callback) {
|
||||
this._community.getGroupJoinRequests(this.steamID, callback);
|
||||
};
|
||||
|
||||
/**
|
||||
* Respond to one or more join requests to this restricted group.
|
||||
* @param {SteamID|string|SteamID[]|string[]} steamIDs - The SteamIDs of the users you want to approve or deny membership for (or a single value)
|
||||
* @param {boolean} approve - True to put them in the group, false to deny their membership
|
||||
* @param {function} callback - Takes only an Error object/null as the first argument
|
||||
*/
|
||||
CSteamGroup.prototype.respondToJoinRequests = function(steamIDs, approve, callback) {
|
||||
this._community.respondToGroupJoinRequests(this.steamID, steamIDs, approve, callback);
|
||||
};
|
||||
|
||||
/**
|
||||
* Respond to *ALL* pending group-join requests for this group.
|
||||
* @param {boolean} approve - True to allow everyone who requested into the group, false to not
|
||||
* @param {function} callback - Takes only an Error object/null as the first argument
|
||||
*/
|
||||
CSteamGroup.prototype.respondToAllJoinRequests = function(approve, callback) {
|
||||
this._community.respondToAllGroupJoinRequests(this.steamID, approve, callback);
|
||||
};
|
||||
|
||||
213
classes/CSteamSharedFile.js
Normal file
213
classes/CSteamSharedFile.js
Normal file
@@ -0,0 +1,213 @@
|
||||
const Cheerio = require('cheerio');
|
||||
const SteamID = require('steamid');
|
||||
|
||||
const SteamCommunity = require('../index.js');
|
||||
const Helpers = require('../components/helpers.js');
|
||||
|
||||
const ESharedFileType = require('../resources/ESharedFileType.js');
|
||||
|
||||
|
||||
/**
|
||||
* Scrape a sharedfile's DOM to get all available information
|
||||
* @param {string} sharedFileId - ID of the sharedfile
|
||||
* @param {function} callback - First argument is null/Error, second is object containing all available information
|
||||
*/
|
||||
SteamCommunity.prototype.getSteamSharedFile = function(sharedFileId, callback) {
|
||||
// Construct object holding all the data we can scrape
|
||||
let sharedfile = {
|
||||
id: sharedFileId,
|
||||
type: null,
|
||||
appID: null,
|
||||
owner: null,
|
||||
fileSize: null,
|
||||
postDate: null,
|
||||
resolution: null,
|
||||
uniqueVisitorsCount: null,
|
||||
favoritesCount: null,
|
||||
upvoteCount: null,
|
||||
guideNumRatings: null,
|
||||
isUpvoted: null,
|
||||
isDownvoted: null
|
||||
};
|
||||
|
||||
// Get DOM of sharedfile
|
||||
this.httpRequestGet(`https://steamcommunity.com/sharedfiles/filedetails/?id=${sharedFileId}`, async (err, res, body) => {
|
||||
try {
|
||||
|
||||
/* --------------------- Preprocess output --------------------- */
|
||||
|
||||
// Load output into cheerio to make parsing easier
|
||||
let $ = Cheerio.load(body);
|
||||
|
||||
// Dynamically map detailsStatsContainerLeft to detailsStatsContainerRight in an object to make readout easier. It holds size, post date and resolution.
|
||||
let detailsStatsObj = {};
|
||||
let detailsLeft = $(".detailsStatsContainerLeft").children();
|
||||
let detailsRight = $(".detailsStatsContainerRight").children();
|
||||
|
||||
Object.keys(detailsLeft).forEach((e) => { // Dynamically get all details. Don't hardcore so that this also works for guides.
|
||||
if (isNaN(e)) {
|
||||
return; // Ignore invalid entries
|
||||
}
|
||||
|
||||
detailsStatsObj[detailsLeft[e].children[0].data.trim()] = detailsRight[e].children[0].data;
|
||||
});
|
||||
|
||||
// Dynamically map stats_table descriptions to values. This holds Unique Visitors and Current Favorites
|
||||
let statsTableObj = {};
|
||||
let statsTable = $(".stats_table").children();
|
||||
|
||||
Object.keys(statsTable).forEach((e) => {
|
||||
if (isNaN(e)) {
|
||||
return; // Ignore invalid entries
|
||||
}
|
||||
|
||||
// Value description is at index 3, value data at index 1
|
||||
statsTableObj[statsTable[e].children[3].children[0].data] = statsTable[e].children[1].children[0].data.replace(/,/g, ""); // Remove commas from 1k+ values
|
||||
});
|
||||
|
||||
|
||||
/* --------------------- Find and map values --------------------- */
|
||||
|
||||
// Find appID in share button onclick event
|
||||
sharedfile.appID = Number($("#ShareItemBtn").attr()["onclick"].replace(`ShowSharePublishedFilePopup( '${sharedFileId}', '`, "").replace("' );", ""));
|
||||
|
||||
|
||||
// Find fileSize if not guide
|
||||
sharedfile.fileSize = detailsStatsObj["File Size"] || null; // TODO: Convert to bytes? It seems like to always be MB but no guarantee
|
||||
|
||||
|
||||
// Find postDate and convert to timestamp
|
||||
let posted = detailsStatsObj["Posted"].trim();
|
||||
|
||||
sharedfile.postDate = Helpers.decodeSteamTime(posted);
|
||||
|
||||
|
||||
// Find resolution if artwork or screenshot
|
||||
sharedfile.resolution = detailsStatsObj["Size"] || null;
|
||||
|
||||
|
||||
// Find uniqueVisitorsCount. We can't use ' || null' here as Number("0") casts to false
|
||||
if (statsTableObj["Unique Visitors"]) {
|
||||
sharedfile.uniqueVisitorsCount = Number(statsTableObj["Unique Visitors"]);
|
||||
}
|
||||
|
||||
|
||||
// Find favoritesCount. We can't use ' || null' here as Number("0") casts to false
|
||||
if (statsTableObj["Current Favorites"]) {
|
||||
sharedfile.favoritesCount = Number(statsTableObj["Current Favorites"]);
|
||||
}
|
||||
|
||||
|
||||
// Find upvoteCount. We can't use ' || null' here as Number("0") casts to false
|
||||
let upvoteCount = $("#VotesUpCountContainer > #VotesUpCount").text();
|
||||
|
||||
if (upvoteCount) {
|
||||
sharedfile.upvoteCount = Number(upvoteCount);
|
||||
}
|
||||
|
||||
|
||||
// Find numRatings if this is a guide as they use a different voting system
|
||||
let numRatings = $(".ratingSection > .numRatings").text().replace(" ratings", "");
|
||||
|
||||
sharedfile.guideNumRatings = Number(numRatings) || null; // Set to null if not a guide or if the guide does not have enough ratings to show a value
|
||||
|
||||
|
||||
// Determine if this account has already voted on this sharedfile
|
||||
sharedfile.isUpvoted = String($(".workshopItemControlCtn > #VoteUpBtn")[0].attribs["class"]).includes("toggled"); // Check if upvote btn class contains "toggled"
|
||||
sharedfile.isDownvoted = String($(".workshopItemControlCtn > #VoteDownBtn")[0].attribs["class"]).includes("toggled"); // Check if downvote btn class contains "toggled"
|
||||
|
||||
|
||||
// Determine type by looking at the second breadcrumb. Find the first separator as it has a unique name and go to the next element which holds our value of interest
|
||||
let breadcrumb = $(".breadcrumbs > .breadcrumb_separator").next().get(0).children[0].data || "";
|
||||
|
||||
if (breadcrumb.includes("Screenshot")) {
|
||||
sharedfile.type = ESharedFileType.Screenshot;
|
||||
}
|
||||
|
||||
if (breadcrumb.includes("Artwork")) {
|
||||
sharedfile.type = ESharedFileType.Artwork;
|
||||
}
|
||||
|
||||
if (breadcrumb.includes("Guide")) {
|
||||
sharedfile.type = ESharedFileType.Guide;
|
||||
}
|
||||
|
||||
|
||||
// Find owner profile link, convert to steamID64 using SteamIdResolver lib and create a SteamID object
|
||||
let ownerHref = $(".friendBlockLinkOverlay").attr()["href"];
|
||||
|
||||
let {steamID} = await this._resolveVanityURL(ownerHref);
|
||||
sharedfile.owner = steamID;
|
||||
|
||||
callback(null, new CSteamSharedFile(this, sharedfile));
|
||||
} catch (err) {
|
||||
callback(err, null);
|
||||
}
|
||||
}, "steamcommunity");
|
||||
};
|
||||
|
||||
/**
|
||||
* Constructor - Creates a new SharedFile object
|
||||
* @class
|
||||
* @param {SteamCommunity} community
|
||||
* @param {{ id: string, type: ESharedFileType, appID: number, owner: SteamID|null, fileSize: string|null, postDate: number, resolution: string|null, uniqueVisitorsCount: number, favoritesCount: number, upvoteCount: number|null, guideNumRatings: Number|null, isUpvoted: boolean, isDownvoted: boolean }} data
|
||||
*/
|
||||
function CSteamSharedFile(community, data) {
|
||||
/**
|
||||
* @type {SteamCommunity}
|
||||
*/
|
||||
this._community = community;
|
||||
|
||||
// Clone all the data we received
|
||||
Object.assign(this, data);
|
||||
}
|
||||
|
||||
/**
|
||||
* Deletes a comment from this sharedfile's comment section
|
||||
* @param {String} cid - ID of the comment to delete
|
||||
* @param {function} callback - Takes only an Error object/null as the first argument
|
||||
*/
|
||||
CSteamSharedFile.prototype.deleteComment = function(cid, callback) {
|
||||
this._community.deleteSharedFileComment(this.owner, this.id, cid, callback);
|
||||
};
|
||||
|
||||
/**
|
||||
* Favorites this sharedfile
|
||||
* @param {function} callback - Takes only an Error object/null as the first argument
|
||||
*/
|
||||
CSteamSharedFile.prototype.favorite = function(callback) {
|
||||
this._community.favoriteSharedFile(this.id, this.appID, callback);
|
||||
};
|
||||
|
||||
/**
|
||||
* Posts a comment to this sharedfile
|
||||
* @param {String} message - Content of the comment to post
|
||||
* @param {function} callback - Takes only an Error object/null as the first argument
|
||||
*/
|
||||
CSteamSharedFile.prototype.comment = function(message, callback) {
|
||||
this._community.postSharedFileComment(this.owner, this.id, message, callback);
|
||||
};
|
||||
|
||||
/**
|
||||
* Subscribes to this sharedfile's comment section. Note: Checkbox on webpage does not update
|
||||
* @param {function} callback - Takes only an Error object/null as the first argument
|
||||
*/
|
||||
CSteamSharedFile.prototype.subscribe = function(callback) {
|
||||
this._community.subscribeSharedFileComments(this.owner, this.id, callback);
|
||||
};
|
||||
|
||||
/**
|
||||
* Unfavorites this sharedfile
|
||||
* @param {function} callback - Takes only an Error object/null as the first argument
|
||||
*/
|
||||
CSteamSharedFile.prototype.unfavorite = function(callback) {
|
||||
this._community.unfavoriteSharedFile(this.id, this.appID, callback);
|
||||
};
|
||||
|
||||
/**
|
||||
* Unsubscribes from this sharedfile's comment section. Note: Checkbox on webpage does not update
|
||||
* @param {function} callback - Takes only an Error object/null as the first argument
|
||||
*/
|
||||
CSteamSharedFile.prototype.unsubscribe = function(callback) {
|
||||
this._community.unsubscribeSharedFileComments(this.owner, this.id, callback);
|
||||
};
|
||||
@@ -1,104 +1,126 @@
|
||||
var SteamCommunity = require('../index.js');
|
||||
var SteamID = require('steamid');
|
||||
var xml2js = require('xml2js');
|
||||
const SteamID = require('steamid');
|
||||
const XML2JS = require('xml2js');
|
||||
|
||||
const Helpers = require('../components/helpers.js');
|
||||
const SteamCommunity = require('../index.js');
|
||||
|
||||
SteamCommunity.prototype.getSteamUser = function(id, callback) {
|
||||
if(typeof id !== 'string' && !(typeof id === 'object' && id.__proto__ === SteamID.prototype)) {
|
||||
throw new Error("id parameter should be a user URL string or a SteamID object");
|
||||
if (typeof id !== 'string' && !Helpers.isSteamID(id)) {
|
||||
throw new Error('id parameter should be a user URL string or a SteamID object');
|
||||
}
|
||||
|
||||
if(typeof id === 'object' && (id.universe != SteamID.Universe.PUBLIC || id.type != SteamID.Type.INDIVIDUAL)) {
|
||||
throw new Error("SteamID must stand for an individual account in the public universe");
|
||||
|
||||
if (typeof id === 'object' && (id.universe != SteamID.Universe.PUBLIC || id.type != SteamID.Type.INDIVIDUAL)) {
|
||||
throw new Error('SteamID must stand for an individual account in the public universe');
|
||||
}
|
||||
|
||||
var self = this;
|
||||
this.request("http://steamcommunity.com/" + (typeof id === 'string' ? "id/" + id : "profiles/" + id.toString()) + "/?xml=1", function(err, response, body) {
|
||||
if(self._checkHttpError(err, response, callback)) {
|
||||
|
||||
this.httpRequest('http://steamcommunity.com/' + (typeof id === 'string' ? 'id/' + id : 'profiles/' + id.toString()) + '/?xml=1', (err, response, body) => {
|
||||
if (err) {
|
||||
callback(err);
|
||||
return;
|
||||
}
|
||||
|
||||
if(self._checkCommunityError(body, callback)) {
|
||||
return;
|
||||
}
|
||||
|
||||
xml2js.parseString(body, function(err, result) {
|
||||
if(err || (!result.response && !result.profile)) {
|
||||
callback(err || new Error("No valid response"));
|
||||
|
||||
XML2JS.parseString(body, (err, result) => {
|
||||
if (err || (!result.response && !result.profile)) {
|
||||
callback(err || new Error('No valid response'));
|
||||
return;
|
||||
}
|
||||
|
||||
if(result.response && result.response.error && result.response.error.length) {
|
||||
|
||||
if (result.response && result.response.error && result.response.error.length) {
|
||||
callback(new Error(result.response.error[0]));
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
// Try and find custom URL from redirect
|
||||
var customurl = null;
|
||||
if(response.request.redirects && response.request.redirects.length) {
|
||||
var match = response.request.redirects[0].redirectUri.match(/https?:\/\/steamcommunity\.com\/id\/([^/])+\/\?xml=1/);
|
||||
if(match) {
|
||||
let customurl = null;
|
||||
if (response.request.redirects && response.request.redirects.length) {
|
||||
let match = response.request.redirects[0].redirectUri.match(/https?:\/\/steamcommunity\.com\/id\/([^/])+\/\?xml=1/);
|
||||
if (match) {
|
||||
customurl = match[1];
|
||||
}
|
||||
}
|
||||
|
||||
callback(null, new CSteamUser(self, result.profile, customurl));
|
||||
|
||||
if (!result.profile.steamID64) {
|
||||
callback(new Error('No valid response'));
|
||||
return;
|
||||
}
|
||||
|
||||
callback(null, new CSteamUser(this, result.profile, customurl));
|
||||
});
|
||||
});
|
||||
}, 'steamcommunity');
|
||||
};
|
||||
|
||||
function CSteamUser(community, userData, customurl) {
|
||||
this._community = community;
|
||||
|
||||
|
||||
this.steamID = new SteamID(userData.steamID64[0]);
|
||||
this.name = userData.steamID[0];
|
||||
this.onlineState = userData.onlineState[0];
|
||||
this.stateMessage = userData.stateMessage[0];
|
||||
this.privacyState = userData.privacyState[0];
|
||||
this.visibilityState = userData.visibilityState[0];
|
||||
this.avatarHash = userData.avatarIcon[0].match(/([0-9a-f]+)\.[a-z]+$/)[1];
|
||||
this.vacBanned = !!userData.vacBanned[0];
|
||||
this.tradeBanState = userData.tradeBanState[0];
|
||||
this.isLimitedAccount = !!userData.isLimitedAccount[0];
|
||||
this.customURL = userData.customURL ? userData.customURL[0] : customurl;
|
||||
|
||||
if(this.visibilityState == 3) {
|
||||
this.memberSince = new Date(userData.memberSince[0].replace(/(\d{1,2})(st|nd|th)/, "$1"));
|
||||
this.location = userData.location[0] || null;
|
||||
this.realName = userData.realname[0] || null;
|
||||
this.summary = userData.summary[0] || null;
|
||||
this.name = processItem('steamID');
|
||||
this.onlineState = processItem('onlineState');
|
||||
this.stateMessage = processItem('stateMessage');
|
||||
this.privacyState = processItem('privacyState', 'uncreated');
|
||||
this.visibilityState = processItem('visibilityState');
|
||||
this.avatarHash = processItem('avatarIcon', '').match(/([0-9a-f]+)\.[a-z]+$/);
|
||||
if (this.avatarHash) {
|
||||
this.avatarHash = this.avatarHash[1];
|
||||
}
|
||||
|
||||
this.vacBanned = processItem('vacBanned', false) == 1;
|
||||
this.tradeBanState = processItem('tradeBanState', 'None');
|
||||
this.isLimitedAccount = processItem('isLimitedAccount') == 1;
|
||||
this.customURL = processItem('customURL', customurl);
|
||||
|
||||
if (this.visibilityState == SteamCommunity.PrivacyState.Public) {
|
||||
let memberSinceValue = processItem('memberSince', '0').replace(/(\d{1,2})(st|nd|th)/, '$1');
|
||||
|
||||
if (memberSinceValue.indexOf(',') === -1) {
|
||||
memberSinceValue += ', ' + new Date().getFullYear();
|
||||
}
|
||||
|
||||
this.memberSince = new Date(memberSinceValue);
|
||||
this.location = processItem('location');
|
||||
this.realName = processItem('realname');
|
||||
this.summary = processItem('summary');
|
||||
} else {
|
||||
this.memberSince = null;
|
||||
this.location = null;
|
||||
this.realName = null;
|
||||
this.summary = null;
|
||||
}
|
||||
|
||||
|
||||
// Maybe handle mostPlayedGames?
|
||||
|
||||
|
||||
this.groups = null;
|
||||
this.primaryGroup = null;
|
||||
|
||||
var self = this;
|
||||
if(userData.groups && userData.groups[0] && userData.groups[0].group) {
|
||||
this.groups = userData.groups[0].group.map(function(group) {
|
||||
if(group['$'] && group['$'].isPrimary) {
|
||||
self.primaryGroup = new SteamID(group.groupID64[0]);
|
||||
|
||||
if (userData.groups && userData.groups[0] && userData.groups[0].group) {
|
||||
this.groups = userData.groups[0].group.map((group) => {
|
||||
if (group.$ && group.$.isPrimary === '1') {
|
||||
this.primaryGroup = new SteamID(group.groupID64[0]);
|
||||
}
|
||||
|
||||
|
||||
return new SteamID(group.groupID64[0]);
|
||||
});
|
||||
}
|
||||
|
||||
function processItem(name, defaultVal) {
|
||||
if (!userData[name]) {
|
||||
return defaultVal;
|
||||
}
|
||||
|
||||
return userData[name][0];
|
||||
}
|
||||
}
|
||||
|
||||
CSteamUser.getAvatarURL = function(hash, size, protocol) {
|
||||
size = size || '';
|
||||
protocol = protocol || 'http://';
|
||||
|
||||
var url = protocol + "steamcdn-a.akamaihd.net/steamcommunity/public/images/avatars/" + hash.substring(0, 2) + "/" + hash;
|
||||
if(size == 'full' || size == 'medium') {
|
||||
return url + "_" + size + ".jpg";
|
||||
|
||||
hash = hash || '72f78b4c8cc1f62323f8a33f6d53e27db57c2252'; // The default "?" avatar
|
||||
|
||||
let url = protocol + 'steamcdn-a.akamaihd.net/steamcommunity/public/images/avatars/' + hash.substring(0, 2) + '/' + hash;
|
||||
if (size == 'full' || size == 'medium') {
|
||||
return url + '_' + size + '.jpg';
|
||||
} else {
|
||||
return url + ".jpg";
|
||||
return url + '.jpg';
|
||||
}
|
||||
};
|
||||
|
||||
@@ -107,120 +129,88 @@ CSteamUser.prototype.getAvatarURL = function(size, protocol) {
|
||||
};
|
||||
|
||||
CSteamUser.prototype.addFriend = function(callback) {
|
||||
this._community.request.post('https://steamcommunity.com/actions/AddFriendAjax', {"form": {"accept_invite": 0, "sessionID": this._community.getSessionID(), "steamid": this.steamID.toString()}}, function(err, response, body) {
|
||||
if(!callback) {
|
||||
return;
|
||||
}
|
||||
|
||||
if(self._checkHttpError(err, response, callback)) {
|
||||
return;
|
||||
}
|
||||
|
||||
var json;
|
||||
try {
|
||||
json = JSON.parse(body);
|
||||
} catch(e) {
|
||||
callback(e);
|
||||
return;
|
||||
}
|
||||
|
||||
if(json.success) {
|
||||
callback(null);
|
||||
} else {
|
||||
callback(new Error("Unknown error"));
|
||||
}
|
||||
});
|
||||
this._community.addFriend(this.steamID, callback);
|
||||
};
|
||||
|
||||
CSteamUser.prototype.acceptFriendRequest = function(callback) {
|
||||
this._community.request.post('https://steamcommunity.com/actions/AddFriendAjax', {"form": {"accept_invite": 1, "sessionID": this._community.getSessionID(), "steamid": this.steamID.toString()}}, function(err, response, body) {
|
||||
if(!callback) {
|
||||
return;
|
||||
}
|
||||
|
||||
if(self._checkHttpError(err, response, callback)) {
|
||||
return;
|
||||
}
|
||||
|
||||
callback(null);
|
||||
});
|
||||
this._community.acceptFriendRequest(this.steamID, callback);
|
||||
};
|
||||
|
||||
CSteamUser.prototype.removeFriend = function(callback) {
|
||||
this._community.request.post('https://steamcommunity.com/actions/RemoveFriendAjax', {"form": {"sessionID": this._community.getSessionID(), "steamid": this.steamID.toString()}}, function(err, response, body) {
|
||||
if(!callback) {
|
||||
return;
|
||||
}
|
||||
|
||||
if(self._checkHttpError(err, response, callback)) {
|
||||
return;
|
||||
}
|
||||
|
||||
callback(null);
|
||||
});
|
||||
this._community.removeFriend(this.steamID, callback);
|
||||
|
||||
};
|
||||
|
||||
CSteamUser.prototype.blockCommunication = function(callback) {
|
||||
this._community.request.post('https://steamcommunity.com/actions/BlockUserAjax', {"form": {"sessionID": this._community.getSessionID(), "steamid": this.steamID.toString()}}, function(err, response, body) {
|
||||
if(!callback) {
|
||||
return;
|
||||
}
|
||||
|
||||
if(self._checkHttpError(err, response, callback)) {
|
||||
return;
|
||||
}
|
||||
|
||||
callback(null);
|
||||
});
|
||||
this._community.blockCommunication(this.steamID, callback);
|
||||
};
|
||||
|
||||
CSteamUser.prototype.unblockCommunication = function(callback) {
|
||||
var form = {"action": "unignore"};
|
||||
form['friends[' + this.steamID.toString() + ']'] = 1;
|
||||
|
||||
this._community._myProfile('friends/blocked/', form, function(err, response, body) {
|
||||
if(!callback) {
|
||||
return;
|
||||
}
|
||||
|
||||
if(err || response.statusCode >= 400) {
|
||||
callback(err || new Error("HTTP error " + response.statusCode));
|
||||
return;
|
||||
}
|
||||
|
||||
callback(null);
|
||||
});
|
||||
this._community.unblockCommunication(this.steamID, callback);
|
||||
};
|
||||
|
||||
CSteamUser.prototype.comment = function(message, callback) {
|
||||
this._community.request.post('https://steamcommunity.com/comment/Profile/post/' + this.steamID.toString() + '/-1/', {"form": {
|
||||
"comment": message,
|
||||
"count": 6,
|
||||
"sessionid": this._community.getSessionID()
|
||||
}}, function(err, response, body) {
|
||||
if(!callback) {
|
||||
return;
|
||||
}
|
||||
|
||||
if(err || response.statusCode != 200) {
|
||||
callback(err || new Error("HTTP error " + response.statusCode));
|
||||
return;
|
||||
}
|
||||
|
||||
var json;
|
||||
try {
|
||||
json = JSON.parse(body);
|
||||
} catch(e) {
|
||||
callback(e);
|
||||
return;
|
||||
}
|
||||
|
||||
if(json.success) {
|
||||
callback(null);
|
||||
} else if(json.error) {
|
||||
callback(new Error(json.error));
|
||||
} else {
|
||||
callback(new Error("Unknown error"));
|
||||
}
|
||||
});
|
||||
this._community.postUserComment(this.steamID, message, callback);
|
||||
};
|
||||
|
||||
CSteamUser.prototype.deleteComment = function(commentID, callback) {
|
||||
this._community.deleteUserComment(this.steamID, commentID, callback);
|
||||
};
|
||||
|
||||
CSteamUser.prototype.getComments = function(options, callback) {
|
||||
this._community.getUserComments(this.steamID, options, callback);
|
||||
};
|
||||
|
||||
CSteamUser.prototype.inviteToGroup = function(groupID, callback) {
|
||||
this._community.inviteUserToGroup(this.steamID, groupID, callback);
|
||||
};
|
||||
|
||||
CSteamUser.prototype.getAliases = function(callback) {
|
||||
this._community.getUserAliases(this.steamID, callback);
|
||||
};
|
||||
|
||||
CSteamUser.prototype.getInventoryContexts = function(callback) {
|
||||
this._community.getUserInventoryContexts(this.steamID, callback);
|
||||
};
|
||||
|
||||
/**
|
||||
* Get the contents of a user's inventory context.
|
||||
* @deprecated Use CSteamUser#getInventoryContents instead
|
||||
* @param {int} appID - The Steam application ID of the game for which you want an inventory
|
||||
* @param {int} contextID - The ID of the "context" within the game you want to retrieve
|
||||
* @param {boolean} tradableOnly - true to get only tradable items and currencies
|
||||
* @param callback
|
||||
*/
|
||||
CSteamUser.prototype.getInventory = function(appID, contextID, tradableOnly, callback) {
|
||||
this._community.getUserInventory(this.steamID, appID, contextID, tradableOnly, callback);
|
||||
};
|
||||
|
||||
/**
|
||||
* Get the contents of a user's inventory context.
|
||||
* @param {int} appID - The Steam application ID of the game for which you want an inventory
|
||||
* @param {int} contextID - The ID of the "context" within the game you want to retrieve
|
||||
* @param {boolean} tradableOnly - true to get only tradable items and currencies
|
||||
* @param {string} [language] - The language of item descriptions to return. Omit for default (which may either be English or your account's chosen language)
|
||||
* @param callback
|
||||
*/
|
||||
CSteamUser.prototype.getInventoryContents = function(appID, contextID, tradableOnly, language, callback) {
|
||||
this._community.getUserInventoryContents(this.steamID, appID, contextID, tradableOnly, language, callback);
|
||||
};
|
||||
|
||||
/**
|
||||
* Get the background URL of user's profile.
|
||||
* @param {function} callback
|
||||
*/
|
||||
CSteamUser.prototype.getProfileBackground = function(callback) {
|
||||
this._community.getUserProfileBackground(this.steamID, callback);
|
||||
};
|
||||
|
||||
/**
|
||||
* Upload an image to Steam and send it to the target user over chat.
|
||||
* @param {Buffer} imageContentsBuffer - The image contents, as a Buffer
|
||||
* @param {{spoiler?: boolean}} [options]
|
||||
* @param {function} callback
|
||||
*/
|
||||
CSteamUser.prototype.sendImage = function(imageContentsBuffer, options, callback) {
|
||||
this._community.sendImageToUser(this.steamID, imageContentsBuffer, options, callback);
|
||||
};
|
||||
|
||||
@@ -1,253 +0,0 @@
|
||||
var SteamCommunity = require('../index.js');
|
||||
var SteamID = require('steamid');
|
||||
|
||||
SteamCommunity.ChatState = {
|
||||
"Offline": 0,
|
||||
"LoggingOn": 1,
|
||||
"LogOnFailed": 2,
|
||||
"LoggedOn": 3
|
||||
};
|
||||
|
||||
SteamCommunity.PersonaState = {
|
||||
"Offline": 0,
|
||||
"Online": 1,
|
||||
"Busy": 2,
|
||||
"Away": 3,
|
||||
"Snooze": 4,
|
||||
"LookingToTrade": 5,
|
||||
"LookingToPlay": 6,
|
||||
"Max": 7
|
||||
};
|
||||
|
||||
SteamCommunity.PersonaStateFlag = {
|
||||
"HasRichPresence": 1,
|
||||
"InJoinableGame": 2,
|
||||
|
||||
"OnlineUsingWeb": 256,
|
||||
"OnlineUsingMobile": 512,
|
||||
"OnlineUsingBigPicture": 1024
|
||||
};
|
||||
|
||||
SteamCommunity.prototype.chatLogon = function(interval, uiMode) {
|
||||
if(this.chatState == SteamCommunity.ChatState.LoggingOn || this.chatState == SteamCommunity.ChatState.LoggedOn) {
|
||||
return;
|
||||
}
|
||||
|
||||
interval = interval || 500;
|
||||
uiMode = uiMode || "web";
|
||||
|
||||
this.emit('debug', 'Requesting chat WebAPI token');
|
||||
this.chatState = SteamCommunity.ChatState.LoggingOn;
|
||||
|
||||
var self = this;
|
||||
this.request("https://steamcommunity.com/chat", function(err, response, body) {
|
||||
if(err || response.statusCode != 200) {
|
||||
self.emit('debug', 'Error requesting chat WebAPI token: ' + (err ? err.message : "HTTP error " + response.statusCode));
|
||||
self.chatState = SteamCommunity.ChatState.LogOnFailed;
|
||||
setTimeout(self.chatLogon.bind(self), 5000);
|
||||
return;
|
||||
}
|
||||
|
||||
var match = body.match(/[0-9a-f]{32}/);
|
||||
if(!match) {
|
||||
self.emit('debug', 'Couldn\'t find a WebAPI chat token in the response.');
|
||||
self.chatState = SteamCommunity.ChatState.LogOnFailed;
|
||||
setTimeout(self.chatLogon.bind(self), 5000);
|
||||
return;
|
||||
}
|
||||
|
||||
self.request.post({
|
||||
"uri": "https://api.steampowered.com/ISteamWebUserPresenceOAuth/Logon/v1",
|
||||
"form": {
|
||||
"ui_mode": uiMode,
|
||||
"access_token": match[0]
|
||||
},
|
||||
"json": true
|
||||
}, function(err, response, body) {
|
||||
if(err || response.statusCode != 200) {
|
||||
self.emit('debug', 'Error logging into webchat: ' + (err ? err.message : "HTTP error " + response.statusCode));
|
||||
self.chatState = SteamCommunity.ChatState.LogOnFailed;
|
||||
setTimeout(self.chatLogon.bind(self), 5000);
|
||||
return;
|
||||
}
|
||||
|
||||
if(body.error != 'OK') {
|
||||
self.emit('debug', 'Error logging into webchat: ' + body.error);
|
||||
self.chatState = SteamCommunity.ChatState.LogOnFailed;
|
||||
setTimeout(self.chatLogon.bind(self), 5000);
|
||||
return;
|
||||
}
|
||||
|
||||
self._chat = {
|
||||
"umqid": body.umqid,
|
||||
"message": body.message,
|
||||
"accessToken": match[0],
|
||||
"interval": interval
|
||||
};
|
||||
|
||||
self.chatFriends = {};
|
||||
|
||||
self.chatState = SteamCommunity.ChatState.LoggedOn;
|
||||
self.emit('chatLoggedOn');
|
||||
self._chatPoll();
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
SteamCommunity.prototype.chatMessage = function(recipient, text, type, callback) {
|
||||
if(this.chatState != SteamCommunity.ChatState.LoggedOn) {
|
||||
throw new Error("Chat must be logged on before messages can be sent");
|
||||
}
|
||||
|
||||
if(typeof recipient === 'string') {
|
||||
recipient = new SteamID(recipient);
|
||||
}
|
||||
|
||||
if(typeof type === 'function') {
|
||||
callback = type;
|
||||
type = 'saytext';
|
||||
}
|
||||
|
||||
type = type || 'saytext';
|
||||
|
||||
this.request.post({
|
||||
"uri": "https://api.steampowered.com/ISteamWebUserPresenceOAuth/Message/v1",
|
||||
"form": {
|
||||
"access_token": this._chat.accessToken,
|
||||
"steamid_dst": recipient.toString(),
|
||||
"text": text,
|
||||
"type": type,
|
||||
"umqid": this._chat.umqid
|
||||
},
|
||||
"json": true
|
||||
}, function(err, response, body) {
|
||||
if(!callback) {
|
||||
return;
|
||||
}
|
||||
|
||||
if(self._checkHttpError(err, response, callback)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if(body.error != 'OK') {
|
||||
callback(new Error(body.error));
|
||||
} else {
|
||||
callback(null);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
SteamCommunity.prototype.chatLogoff = function() {
|
||||
var self = this;
|
||||
this.request.post({
|
||||
"uri": "https://api.steampowered.com/ISteamWebUserPresenceOAuth/Logoff/v1",
|
||||
"form": {
|
||||
"access_token": this._chat.accessToken,
|
||||
"umqid": this._chat.umqid
|
||||
}
|
||||
}, function(err, response, body) {
|
||||
if(err || response.statusCode != 200) {
|
||||
self.emit('debug', 'Error logging off of chat: ' + (err ? err.message : "HTTP error " + response.statusCode));
|
||||
setTimeout(self.chatLogoff.bind(self), 1000);
|
||||
} else {
|
||||
self.emit('chatLoggedOff');
|
||||
clearTimeout(self._chat.timer);
|
||||
delete self._chat;
|
||||
delete self.chatFriends;
|
||||
self.chatState = SteamCommunity.ChatState.Offline;
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
SteamCommunity.prototype._chatPoll = function() {
|
||||
this.emit('debug', 'Doing chat poll');
|
||||
|
||||
var self = this;
|
||||
this.request.post({
|
||||
"uri": "https://api.steampowered.com/ISteamWebUserPresenceOAuth/Poll/v1",
|
||||
"form": {
|
||||
"umqid": self._chat.umqid,
|
||||
"message": self._chat.message,
|
||||
"pollid": 1,
|
||||
"sectimeout": 20,
|
||||
"secidletime": 0,
|
||||
"use_accountids": 1,
|
||||
"access_token": self._chat.accessToken
|
||||
},
|
||||
"json": true
|
||||
}, function(err, response, body) {
|
||||
if(self.chatState == SteamCommunity.ChatState.Offline) {
|
||||
return;
|
||||
}
|
||||
|
||||
self._chat.timer = setTimeout(self._chatPoll.bind(self), self._chat.interval);
|
||||
|
||||
if(err || response.statusCode != 200) {
|
||||
self.emit('debug', 'Error in chat poll: ' + (err ? err.message : "HTTP error " + response.statusCode));
|
||||
return;
|
||||
}
|
||||
|
||||
if(body.error != 'OK') {
|
||||
self.emit('debug', 'Error in chat poll: ' + body.error);
|
||||
return;
|
||||
}
|
||||
|
||||
self._chat.message = body.messagelast;
|
||||
|
||||
(body.messages || []).forEach(function(message) {
|
||||
var sender = new SteamID();
|
||||
sender.universe = SteamID.Universe.PUBLIC;
|
||||
sender.type = SteamID.Type.INDIVIDUAL;
|
||||
sender.instance = SteamID.Instance.DESKTOP;
|
||||
sender.accountid = message.accountid_from;
|
||||
|
||||
switch(message.type) {
|
||||
case 'personastate':
|
||||
self._chatUpdatePersona(sender);
|
||||
break;
|
||||
|
||||
case 'saytext':
|
||||
self.emit('chatMessage', sender, message.text);
|
||||
break;
|
||||
|
||||
case 'typing':
|
||||
self.emit('chatTyping', sender);
|
||||
break;
|
||||
|
||||
default:
|
||||
self.emit('debug', 'Unhandled chat message type: ' + message.type);
|
||||
}
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
SteamCommunity.prototype._chatUpdatePersona = function(steamID) {
|
||||
this.emit('debug', 'Updating persona data for ' + steamID);
|
||||
var self = this;
|
||||
this.request({
|
||||
"uri": "https://steamcommunity.com/chat/friendstate/" + steamID.accountid,
|
||||
"json": true
|
||||
}, function(err, response, body) {
|
||||
if(err || response.statusCode != 200) {
|
||||
self.emit('debug', 'Chat update persona error: ' + (err ? err.message : "HTTP error " + response.statusCode));
|
||||
setTimeout(function() {
|
||||
self._chatUpdatePersona(steamID);
|
||||
}, 2000);
|
||||
return;
|
||||
}
|
||||
|
||||
var persona = {
|
||||
"steamID": steamID,
|
||||
"personaName": body.m_strName,
|
||||
"personaState": body.m_ePersonaState,
|
||||
"personaStateFlags": body.m_nPersonaStateFlags || 0,
|
||||
"avatarHash": body.m_strAvatarHash,
|
||||
"inGame": !!body.m_bInGame,
|
||||
"inGameAppID": body.m_nInGameAppID ? parseInt(body.m_nInGameAppID, 10) : null,
|
||||
"inGameName": body.m_strInGameName || null
|
||||
};
|
||||
|
||||
self.emit('chatPersonaState', steamID, persona);
|
||||
self.chatFriends[steamID.getSteamID64()] = persona;
|
||||
});
|
||||
};
|
||||
218
components/confirmations.js
Normal file
218
components/confirmations.js
Normal file
@@ -0,0 +1,218 @@
|
||||
const Cheerio = require('cheerio');
|
||||
const StdLib = require('@doctormckay/stdlib');
|
||||
const SteamTotp = require('steam-totp');
|
||||
|
||||
const SteamCommunity = require('../index.js');
|
||||
|
||||
const CConfirmation = require('../classes/CConfirmation.js');
|
||||
const EConfirmationType = SteamCommunity.EConfirmationType;
|
||||
|
||||
/**
|
||||
* Get a list of your account's currently outstanding confirmations.
|
||||
* @param {int} time - The unix timestamp with which the following key was generated
|
||||
* @param {string} key - The confirmation key that was generated using the preceeding time and the tag 'conf' (this key can be reused)
|
||||
* @param {SteamCommunity~getConfirmations} [callback] - Called when the list of confirmations is received
|
||||
* @return Promise<{confirmations: CConfirmation[]}>
|
||||
*/
|
||||
SteamCommunity.prototype.getConfirmations = function(time, key, callback) {
|
||||
return StdLib.Promises.callbackPromise(['confirmations'], callback, false, async (resolve, reject) => {
|
||||
let body = await request(this, 'getlist', key, time, 'list', null);
|
||||
|
||||
if (!body.success) {
|
||||
if (body.needauth) {
|
||||
let err = new Error('Not Logged In');
|
||||
this._notifySessionExpired(err);
|
||||
return reject(err);
|
||||
}
|
||||
|
||||
return reject(new Error(body.message || body.detail || 'Failed to get confirmation list'));
|
||||
}
|
||||
|
||||
let confs = (body.conf || []).map(conf => new CConfirmation(this, {
|
||||
id: conf.id,
|
||||
type: conf.type,
|
||||
creator: conf.creator_id,
|
||||
key: conf.nonce,
|
||||
title: `${conf.type_name || 'Confirm'} - ${conf.headline || ''}`,
|
||||
receiving: conf.type == EConfirmationType.Trade ? ((conf.summary || [])[1] || '') : '',
|
||||
sending: (conf.summary || [])[0] || '',
|
||||
time: (new Date(conf.creation_time * 1000)).toISOString(), // for backward compatibility
|
||||
timestamp: new Date(conf.creation_time * 1000),
|
||||
icon: conf.icon || ''
|
||||
}));
|
||||
|
||||
resolve({confirmations: confs});
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* @callback SteamCommunity~getConfirmations
|
||||
* @param {Error|null} err - An Error object on failure, or null on success
|
||||
* @param {CConfirmation[]} [confirmations] - An array of CConfirmation objects
|
||||
*/
|
||||
|
||||
/**
|
||||
* Get the trade offer ID associated with a particular confirmation
|
||||
* @param {int} confID - The ID of the confirmation in question
|
||||
* @param {int} time - The unix timestamp with which the following key was generated
|
||||
* @param {string} key - The confirmation key that was generated using the preceeding time and the tag "detail" (this key can be reused)
|
||||
* @param {SteamCommunity~getConfirmationOfferID} [callback]
|
||||
* @return Promise<{offerID: string|null}>
|
||||
*/
|
||||
SteamCommunity.prototype.getConfirmationOfferID = function(confID, time, key, callback) {
|
||||
return StdLib.Promises.callbackPromise(['offerID'], callback, false, async (resolve, reject) => {
|
||||
let body = await request(this, 'detailspage/' + confID, key, time, 'detail', null);
|
||||
|
||||
if (typeof body != 'string') {
|
||||
return reject(new Error('Cannot load confirmation details'));
|
||||
}
|
||||
|
||||
let $ = Cheerio.load(body);
|
||||
let offer = $('.tradeoffer');
|
||||
if (offer.length < 1) {
|
||||
return resolve({offerID: null});
|
||||
}
|
||||
|
||||
resolve({offerID: offer.attr('id').split('_')[1]});
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* @callback SteamCommunity~getConfirmationOfferID
|
||||
* @param {Error|null} err - An Error object on failure, or null on success
|
||||
* @param {string} offerID - The trade offer ID associated with the specified confirmation, or null if not for an offer
|
||||
*/
|
||||
|
||||
/**
|
||||
* Confirm or cancel a given confirmation.
|
||||
* @param {int|int[]|string|string[]} confID - The ID of the confirmation in question, or an array of confirmation IDs
|
||||
* @param {string|string[]} confKey - The confirmation key associated with the confirmation in question (or an array of them) (not a TOTP key, the `key` property of CConfirmation)
|
||||
* @param {int} time - The unix timestamp with which the following key was generated
|
||||
* @param {string} key - The confirmation key that was generated using the preceding time and the tag "allow" (if accepting) or "cancel" (if not accepting)
|
||||
* @param {boolean} accept - true if you want to accept the confirmation, false if you want to cancel it
|
||||
* @param {SteamCommunity~genericErrorCallback} [callback] - Called when the request is complete
|
||||
* @return Promise<void>
|
||||
*/
|
||||
SteamCommunity.prototype.respondToConfirmation = function(confID, confKey, time, key, accept, callback) {
|
||||
return StdLib.Promises.callbackPromise(null, callback, true, async (resolve, reject) => {
|
||||
let tag = accept ? 'accept' : 'reject';
|
||||
|
||||
// The official app uses tags reject/accept, but cancel/allow still works so use these for backward compatibility
|
||||
let body = await request(this, (confID instanceof Array) ? 'multiajaxop' : 'ajaxop', key, time, tag, {
|
||||
op: accept ? 'allow' : 'cancel',
|
||||
cid: confID,
|
||||
ck: confKey
|
||||
});
|
||||
|
||||
if (body.success) {
|
||||
return resolve();
|
||||
}
|
||||
|
||||
reject(new Error(body.message || body.detail || 'Could not act on confirmation'));
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Accept a confirmation for a given object (trade offer or market listing) automatically.
|
||||
* @param {string} identitySecret
|
||||
* @param {number|string} objectID
|
||||
* @param {SteamCommunity~genericErrorCallback} [callback]
|
||||
* @return Promise<void>
|
||||
*/
|
||||
SteamCommunity.prototype.acceptConfirmationForObject = function(identitySecret, objectID, callback) {
|
||||
this._usedConfTimes = this._usedConfTimes || [];
|
||||
|
||||
return StdLib.Promises.callbackPromise(null, callback, true, async (resolve, reject) => {
|
||||
// Figure out our time offset
|
||||
if (typeof this._timeOffset == 'undefined') {
|
||||
await new Promise((resolve) => {
|
||||
SteamTotp.getTimeOffset((err, offset) => {
|
||||
if (err) {
|
||||
// not critical that this succeeds
|
||||
return resolve();
|
||||
}
|
||||
|
||||
this._timeOffset = offset;
|
||||
resolve();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
let offset = this._timeOffset;
|
||||
let time = SteamTotp.time(offset);
|
||||
let key = SteamTotp.getConfirmationKey(identitySecret, time, 'list');
|
||||
let {confirmations} = await this.getConfirmations(time, key);
|
||||
|
||||
let conf = confirmations.find(conf => conf.creator == objectID);
|
||||
if (!conf) {
|
||||
return reject(new Error(`Could not find confirmation for object ${objectID}`));
|
||||
}
|
||||
|
||||
// make sure we don't reuse the same time
|
||||
let localOffset = 0;
|
||||
do {
|
||||
time = SteamTotp.time(offset) + localOffset++;
|
||||
} while (this._usedConfTimes.includes(time));
|
||||
|
||||
this._usedConfTimes.push(time);
|
||||
if (this._usedConfTimes.length > 60) {
|
||||
this._usedConfTimes.splice(0, this._usedConfTimes.length - 60); // we don't need to save more than 60 entries
|
||||
}
|
||||
|
||||
await conf.respond(time, SteamTotp.getConfirmationKey(identitySecret, time, 'accept'), true);
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Send a single request to Steam to accept all outstanding confirmations (after loading the list). If one fails, the
|
||||
* entire request will fail and there will be no way to know which failed without loading the list again.
|
||||
* @param {number} time
|
||||
* @param {string} listKey
|
||||
* @param {string} acceptKey
|
||||
* @param {function} [callback]
|
||||
* @return Promise<{confirmations: CConfirmation[]}>
|
||||
*/
|
||||
SteamCommunity.prototype.acceptAllConfirmations = function(time, listKey, acceptKey, callback) {
|
||||
return StdLib.Promises.callbackPromise(null, callback, true, async (resolve, reject) => {
|
||||
let {confirmations} = await this.getConfirmations(time, listKey);
|
||||
|
||||
if (confirmations.length == 0) {
|
||||
return resolve({confirmations: []});
|
||||
}
|
||||
|
||||
let confIds = confirmations.map(conf => conf.id);
|
||||
let confKeys = confirmations.map(conf => conf.key);
|
||||
await this.respondToConfirmation(confIds, confKeys, time, acceptKey, true);
|
||||
|
||||
resolve({confirmations});
|
||||
});
|
||||
};
|
||||
|
||||
async function request(community, url, key, time, tag, params) {
|
||||
if (!community.steamID) {
|
||||
throw new Error('Must be logged in before trying to do anything with confirmations');
|
||||
}
|
||||
|
||||
params = params || {};
|
||||
params.p = SteamTotp.getDeviceID(community.steamID);
|
||||
params.a = community.steamID.getSteamID64();
|
||||
params.k = key;
|
||||
params.t = time;
|
||||
params.m = 'react';
|
||||
params.tag = tag;
|
||||
|
||||
let req = {
|
||||
method: url == 'multiajaxop' ? 'POST' : 'GET',
|
||||
url: `https://steamcommunity.com/mobileconf/${url}`,
|
||||
source: 'steamcommunity'
|
||||
};
|
||||
|
||||
if (req.method == 'GET') {
|
||||
req.qs = params;
|
||||
} else {
|
||||
req.form = params;
|
||||
}
|
||||
|
||||
let result = await community.httpRequest(req);
|
||||
return result.jsonBody || result.textBody;
|
||||
}
|
||||
727
components/groups.js
Normal file
727
components/groups.js
Normal file
@@ -0,0 +1,727 @@
|
||||
const Cheerio = require('cheerio');
|
||||
const SteamID = require('steamid');
|
||||
const XML2JS = require('xml2js');
|
||||
|
||||
const Helpers = require('./helpers.js');
|
||||
const SteamCommunity = require('../index.js');
|
||||
|
||||
const EResult = SteamCommunity.EResult;
|
||||
|
||||
SteamCommunity.prototype.getGroupMembers = function(gid, callback, members, link, addresses, addressIdx) {
|
||||
members = members || [];
|
||||
|
||||
if (!link) {
|
||||
if (typeof gid != 'string') {
|
||||
// It's a SteamID object
|
||||
link = `https://steamcommunity.com/gid/${gid.toString()}/memberslistxml/?xml=1`;
|
||||
} else {
|
||||
try {
|
||||
// new SteamID could throw which is why we have this funky-looking try/catch set up here
|
||||
let sid = new SteamID(gid);
|
||||
if (sid.type == SteamID.Type.CLAN && sid.isValid()) {
|
||||
link = `https://steamcommunity.com/gid/${sid.getSteamID64()}/memberslistxml/?xml=1`;
|
||||
} else {
|
||||
throw new Error('Doesn\'t particularly matter what this message is');
|
||||
}
|
||||
} catch (e) {
|
||||
link = `http://steamcommunity.com/groups/${gid}/memberslistxml/?xml=1`;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
addressIdx = addressIdx || 0;
|
||||
|
||||
let options = {};
|
||||
options.uri = link;
|
||||
|
||||
if (addresses) {
|
||||
if (addressIdx >= addresses.length) {
|
||||
addressIdx = 0;
|
||||
}
|
||||
|
||||
options.localAddress = addresses[addressIdx];
|
||||
}
|
||||
|
||||
this.httpRequest(options, (err, response, body) => {
|
||||
if (err) {
|
||||
callback(err);
|
||||
return;
|
||||
}
|
||||
|
||||
XML2JS.parseString(body, (err, result) => {
|
||||
if (err) {
|
||||
callback(err);
|
||||
return;
|
||||
}
|
||||
|
||||
members = members.concat(result.memberList.members[0].steamID64.map(sid => new SteamID(sid)));
|
||||
|
||||
if (result.memberList.nextPageLink) {
|
||||
addressIdx++;
|
||||
this.getGroupMembers(gid, callback, members, result.memberList.nextPageLink[0], addresses, addressIdx);
|
||||
} else {
|
||||
callback(null, members);
|
||||
}
|
||||
});
|
||||
}, 'steamcommunity');
|
||||
};
|
||||
|
||||
SteamCommunity.prototype.getGroupMembersEx = function(gid, addresses, callback) {
|
||||
this.getGroupMembers(gid, callback, null, null, addresses, 0);
|
||||
};
|
||||
|
||||
SteamCommunity.prototype.joinGroup = function(gid, callback) {
|
||||
if (typeof gid == 'string') {
|
||||
gid = new SteamID(gid);
|
||||
}
|
||||
|
||||
this.httpRequestPost({
|
||||
url: `https://steamcommunity.com/gid/${gid.getSteamID64()}`,
|
||||
form: {
|
||||
action: 'join',
|
||||
sessionID: this.getSessionID()
|
||||
}
|
||||
}, (err, response, body) => {
|
||||
if (!callback) {
|
||||
return;
|
||||
}
|
||||
|
||||
callback(err || null);
|
||||
}, 'steamcommunity');
|
||||
};
|
||||
|
||||
SteamCommunity.prototype.leaveGroup = function(gid, callback) {
|
||||
if (typeof gid == 'string') {
|
||||
gid = new SteamID(gid);
|
||||
}
|
||||
|
||||
this._myProfile('home_process', {
|
||||
sessionID: this.getSessionID(),
|
||||
action: 'leaveGroup',
|
||||
groupId: gid.getSteamID64()
|
||||
}, (err, response, body) => {
|
||||
if (!callback) {
|
||||
return;
|
||||
}
|
||||
|
||||
callback(err || null);
|
||||
});
|
||||
};
|
||||
|
||||
SteamCommunity.prototype.getAllGroupAnnouncements = function(gid, time, callback) {
|
||||
if (typeof gid == 'string') {
|
||||
gid = new SteamID(gid);
|
||||
}
|
||||
|
||||
if (typeof time == 'function') {
|
||||
callback = time;
|
||||
time = new Date(0); // The beginnig of time...
|
||||
}
|
||||
|
||||
this.httpRequest({
|
||||
url: `https://steamcommunity.com/gid/${gid.getSteamID64()}/rss/`
|
||||
}, (err, response, body) => {
|
||||
if (err) {
|
||||
callback(err);
|
||||
return;
|
||||
}
|
||||
|
||||
XML2JS.parseString(body, (err, results) => {
|
||||
if (err) {
|
||||
return callback(err);
|
||||
}
|
||||
|
||||
if (!results.rss.channel[0].item) {
|
||||
return callback(null, []);
|
||||
}
|
||||
|
||||
let announcements = results.rss.channel[0].item.map((announcement) => {
|
||||
let splitLink = announcement.link[0].split('/');
|
||||
return {
|
||||
headline: announcement.title[0],
|
||||
content: announcement.description[0],
|
||||
date: new Date(announcement.pubDate[0]),
|
||||
author: (typeof announcement.author === 'undefined') ? null : announcement.author[0],
|
||||
aid: splitLink[splitLink.length - 1]
|
||||
};
|
||||
}).filter(announcement => announcement.date > time);
|
||||
|
||||
return callback(null, announcements);
|
||||
});
|
||||
}, 'steamcommunity');
|
||||
};
|
||||
|
||||
SteamCommunity.prototype.postGroupAnnouncement = function(gid, headline, content, hidden, callback) {
|
||||
if (typeof gid == 'string') {
|
||||
gid = new SteamID(gid);
|
||||
}
|
||||
|
||||
if (typeof hidden === 'function') {
|
||||
callback = hidden;
|
||||
hidden = false;
|
||||
}
|
||||
|
||||
let form = {
|
||||
sessionID: this.getSessionID(),
|
||||
action: 'post',
|
||||
headline: headline,
|
||||
body: content,
|
||||
'languages[0][headline]': headline,
|
||||
'languages[0][body]': content
|
||||
};
|
||||
|
||||
if (hidden) {
|
||||
form.is_hidden = 'is_hidden';
|
||||
}
|
||||
|
||||
this.httpRequestPost({
|
||||
url: `https://steamcommunity.com/gid/${gid.getSteamID64()}/announcements`,
|
||||
form
|
||||
}, (err, response, body) => {
|
||||
if (!callback) {
|
||||
return;
|
||||
}
|
||||
|
||||
callback(err || null);
|
||||
}, 'steamcommunity');
|
||||
};
|
||||
|
||||
SteamCommunity.prototype.editGroupAnnouncement = function(gid, aid, headline, content, callback) {
|
||||
if (typeof gid == 'string') {
|
||||
gid = new SteamID(gid);
|
||||
}
|
||||
|
||||
let submitData = {
|
||||
url: `https://steamcommunity.com/gid/${gid.getSteamID64()}/announcements`,
|
||||
form: {
|
||||
sessionID: this.getSessionID(),
|
||||
gid: aid,
|
||||
action: 'update',
|
||||
headline: headline,
|
||||
body: content,
|
||||
'languages[0][headline]': headline,
|
||||
'languages[0][body]': content,
|
||||
'languages[0][updated]': 1
|
||||
}
|
||||
};
|
||||
|
||||
this.httpRequestPost(submitData, (err, response, body) => {
|
||||
if (!callback) {
|
||||
return;
|
||||
}
|
||||
|
||||
callback(err || null);
|
||||
}, 'steamcommunity');
|
||||
};
|
||||
|
||||
SteamCommunity.prototype.deleteGroupAnnouncement = function(gid, aid, callback) {
|
||||
if (typeof gid == 'string') {
|
||||
gid = new SteamID(gid);
|
||||
}
|
||||
|
||||
let submitData = {
|
||||
url: `https://steamcommunity.com/gid/${gid.getSteamID64()}/announcements/delete/${aid}?sessionID=${this.getSessionID()}`
|
||||
};
|
||||
|
||||
this.httpRequestGet(submitData, (err, response, body) => {
|
||||
if (!callback) {
|
||||
return;
|
||||
}
|
||||
|
||||
callback(err || null);
|
||||
}, 'steamcommunity');
|
||||
};
|
||||
|
||||
SteamCommunity.prototype.scheduleGroupEvent = function(gid, name, type, description, time, server, callback) {
|
||||
if (typeof gid == 'string') {
|
||||
gid = new SteamID(gid);
|
||||
}
|
||||
|
||||
// Event types: ChatEvent - Chat, OtherEvent - A lil somethin somethin, PartyEvent - Party!, MeetingEvent - Important meeting, SpecialCauseEvent - Special cause (charity ball?), MusicAndArtsEvent - Music or Art type thing, SportsEvent - Sporting endeavor, TripEvent - Out of town excursion
|
||||
// Passing a number for type will make it a game event for that appid
|
||||
|
||||
switch (typeof server) {
|
||||
case 'function':
|
||||
callback = server;
|
||||
server = {ip: '', password: ''};
|
||||
break;
|
||||
|
||||
case 'string':
|
||||
server = {ip: server, password: ''};
|
||||
break;
|
||||
|
||||
default:
|
||||
if (typeof server != 'object') {
|
||||
server = {ip: '', password: ''};
|
||||
}
|
||||
}
|
||||
|
||||
let form = {
|
||||
sessionid: this.getSessionID(),
|
||||
action: 'newEvent',
|
||||
tzOffset: new Date().getTimezoneOffset() * -60,
|
||||
name: name,
|
||||
type: (typeof type == 'number' || !isNaN(parseInt(type, 10)) ? 'GameEvent' : type),
|
||||
appID: (typeof type == 'number' || !isNaN(parseInt(type, 10)) ? type : ''),
|
||||
serverIP: server.ip,
|
||||
serverPassword: server.password,
|
||||
notes: description,
|
||||
eventQuickTime: 'now'
|
||||
};
|
||||
|
||||
if (time === null) {
|
||||
form.startDate = 'MM/DD/YY';
|
||||
form.startHour = '12';
|
||||
form.startMinute = '00';
|
||||
form.startAMPM = 'PM';
|
||||
form.timeChoice = 'quick';
|
||||
} else {
|
||||
form.startDate = (time.getMonth() + 1 < 10 ? '0' : '') + (time.getMonth() + 1) + '/' + (time.getDate() < 10 ? '0' : '') + time.getDate() + '/' + time.getFullYear().toString().substring(2);
|
||||
form.startHour = (time.getHours() === 0 ? '12' : (time.getHours() > 12 ? time.getHours() - 12 : time.getHours()));
|
||||
form.startMinute = (time.getMinutes() < 10 ? '0' : '') + time.getMinutes();
|
||||
form.startAMPM = (time.getHours() <= 12 ? 'AM' : 'PM');
|
||||
form.timeChoice = 'specific';
|
||||
}
|
||||
|
||||
this.httpRequestPost({
|
||||
url: `https://steamcommunity.com/gid/${gid.toString()}/eventEdit`,
|
||||
form
|
||||
}, (err, response, body) => {
|
||||
if (!callback) {
|
||||
return;
|
||||
}
|
||||
|
||||
callback(err || null);
|
||||
}, 'steamcommunity');
|
||||
};
|
||||
|
||||
SteamCommunity.prototype.editGroupEvent = function(gid, id, name, type, description, time, server, callback) {
|
||||
if (typeof gid === 'string') {
|
||||
gid = new SteamID(gid);
|
||||
}
|
||||
|
||||
// Event types: ChatEvent - Chat, OtherEvent - A lil somethin somethin, PartyEvent - Party!, MeetingEvent - Important meeting, SpecialCauseEvent - Special cause (charity ball?), MusicAndArtsEvent - Music or Art type thing, SportsEvent - Sporting endeavor, TripEvent - Out of town excursion
|
||||
// Passing a number for type will make it a game event for that appid
|
||||
|
||||
switch (typeof server) {
|
||||
case 'function':
|
||||
callback = server;
|
||||
server = {ip: '', password: ''};
|
||||
break;
|
||||
|
||||
case 'string':
|
||||
server = {ip: server, password: ''};
|
||||
break;
|
||||
|
||||
default:
|
||||
if (typeof server != 'object') {
|
||||
server = {ip: '', password: ''};
|
||||
}
|
||||
}
|
||||
|
||||
let form = {
|
||||
sessionid: this.getSessionID(),
|
||||
action: 'updateEvent',
|
||||
eventID: id,
|
||||
tzOffset: new Date().getTimezoneOffset() * -60,
|
||||
name: name,
|
||||
type: (typeof type == 'number' || !isNaN(parseInt(type, 10)) ? 'GameEvent' : type),
|
||||
appID: (typeof type == 'number' || !isNaN(parseInt(type, 10)) ? type : ''),
|
||||
serverIP: server.ip,
|
||||
serverPassword: server.password,
|
||||
notes: description,
|
||||
eventQuickTime: 'now'
|
||||
};
|
||||
|
||||
if (time === null) {
|
||||
form.startDate = 'MM/DD/YY';
|
||||
form.startHour = '12';
|
||||
form.startMinute = '00';
|
||||
form.startAMPM = 'PM';
|
||||
form.timeChoice = 'quick';
|
||||
} else {
|
||||
form.startDate = (time.getMonth() + 1 < 10 ? '0' : '') + (time.getMonth() + 1) + '/' + (time.getDate() < 10 ? '0' : '') + time.getDate() + '/' + time.getFullYear().toString().substring(2);
|
||||
form.startHour = (time.getHours() === 0 ? '12' : (time.getHours() > 12 ? time.getHours() - 12 : time.getHours()));
|
||||
form.startMinute = (time.getMinutes() < 10 ? '0' : '') + time.getMinutes();
|
||||
form.startAMPM = (time.getHours() <= 12 ? 'AM' : 'PM');
|
||||
form.timeChoice = 'specific';
|
||||
}
|
||||
|
||||
this.httpRequestPost({
|
||||
url: `https://steamcommunity.com/gid/${gid.toString()}/eventEdit`,
|
||||
form
|
||||
}, (err, response, body) => {
|
||||
if (!callback) {
|
||||
return;
|
||||
}
|
||||
|
||||
callback(err || null);
|
||||
}, 'steamcommunity');
|
||||
};
|
||||
|
||||
SteamCommunity.prototype.deleteGroupEvent = function(gid, id, callback) {
|
||||
if (typeof gid == 'string') {
|
||||
gid = new SteamID(gid);
|
||||
}
|
||||
|
||||
let form = {
|
||||
sessionid: this.getSessionID(),
|
||||
action: 'deleteEvent',
|
||||
eventID: id
|
||||
};
|
||||
|
||||
this.httpRequestPost({
|
||||
url: `https://steamcommunity.com/gid/${gid.toString()}/eventEdit`,
|
||||
form
|
||||
}, (err, response, body) => {
|
||||
if (!callback) {
|
||||
return;
|
||||
}
|
||||
|
||||
callback(err || null);
|
||||
}, 'steamcommunity');
|
||||
};
|
||||
|
||||
SteamCommunity.prototype.setGroupPlayerOfTheWeek = function(gid, steamID, callback) {
|
||||
if (typeof gid == 'string') {
|
||||
gid = new SteamID(gid);
|
||||
}
|
||||
|
||||
if (typeof steamID == 'string') {
|
||||
steamID = new SteamID(steamID);
|
||||
}
|
||||
|
||||
this.httpRequestPost({
|
||||
url: `https://steamcommunity.com/gid/${gid.getSteamID64()}/potwEdit`,
|
||||
form: {
|
||||
xml: 1,
|
||||
action: 'potw',
|
||||
memberId: steamID.getSteam3RenderedID(),
|
||||
sessionid: this.getSessionID()
|
||||
}
|
||||
}, (err, response, body) => {
|
||||
if (!callback) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (err || response.statusCode != 200) {
|
||||
callback(err || new Error(`HTTP error ${response.statusCode}`));
|
||||
return;
|
||||
}
|
||||
|
||||
XML2JS.parseString(body, (err, results) => {
|
||||
if (err) {
|
||||
callback(err);
|
||||
return;
|
||||
}
|
||||
|
||||
if (results.response.results[0] == 'OK') {
|
||||
callback(null, new SteamID(results.response.oldPOTW[0]), new SteamID(results.response.newPOTW[0]));
|
||||
} else {
|
||||
callback(new Error(results.response.results[0]));
|
||||
}
|
||||
});
|
||||
}, 'steamcommunity');
|
||||
};
|
||||
|
||||
SteamCommunity.prototype.kickGroupMember = function(gid, steamID, callback) {
|
||||
if (typeof gid == 'string') {
|
||||
gid = new SteamID(gid);
|
||||
}
|
||||
|
||||
if (typeof steamID == 'string') {
|
||||
steamID = new SteamID(steamID);
|
||||
}
|
||||
|
||||
this.httpRequestPost({
|
||||
url: `https://steamcommunity.com/gid/${gid.getSteamID64()}/membersManage`,
|
||||
form: {
|
||||
sessionID: this.getSessionID(),
|
||||
action: 'kick',
|
||||
memberId: steamID.getSteamID64(),
|
||||
queryString: ''
|
||||
}
|
||||
}, (err, response, body) => {
|
||||
if (!callback) {
|
||||
return;
|
||||
}
|
||||
|
||||
callback(err || null);
|
||||
}, 'steamcommunity');
|
||||
};
|
||||
|
||||
SteamCommunity.prototype.getGroupHistory = function(gid, page, callback) {
|
||||
if (typeof gid == 'string') {
|
||||
gid = new SteamID(gid);
|
||||
}
|
||||
|
||||
if (typeof page == 'function') {
|
||||
callback = page;
|
||||
page = 1;
|
||||
}
|
||||
|
||||
this.httpRequest(`https://steamcommunity.com/gid/${gid.getSteamID64()}/history?p=${page}`, (err, response, body) => {
|
||||
if (err) {
|
||||
callback(err);
|
||||
return;
|
||||
}
|
||||
|
||||
let $ = Cheerio.load(body);
|
||||
let output = {};
|
||||
|
||||
let paging = $('.group_paging p').text();
|
||||
let match = paging.match(/(\d+) - (\d+) of (\d+)/);
|
||||
|
||||
if (match) {
|
||||
output.first = parseInt(match[1], 10);
|
||||
output.last = parseInt(match[2], 10);
|
||||
output.total = parseInt(match[3], 10);
|
||||
}
|
||||
|
||||
output.items = [];
|
||||
let currentYear = (new Date()).getFullYear();
|
||||
let lastDate = Date.now();
|
||||
|
||||
Array.prototype.forEach.call($('.historyItem, .historyItemb'), (item) => {
|
||||
let data = {};
|
||||
|
||||
let $item = $(item);
|
||||
data.type = $item.find('.historyShort').text().replace(/ /g, '');
|
||||
|
||||
let users = $item.find('.whiteLink[data-miniprofile]');
|
||||
let sid;
|
||||
if (users[0]) {
|
||||
sid = new SteamID();
|
||||
sid.universe = SteamID.Universe.PUBLIC;
|
||||
sid.type = SteamID.Type.INDIVIDUAL;
|
||||
sid.instance = SteamID.Instance.DESKTOP;
|
||||
sid.accountid = $(users[0]).data('miniprofile');
|
||||
data.user = sid;
|
||||
}
|
||||
|
||||
if (users[1]) {
|
||||
sid = new SteamID();
|
||||
sid.universe = SteamID.Universe.PUBLIC;
|
||||
sid.type = SteamID.Type.INDIVIDUAL;
|
||||
sid.instance = SteamID.Instance.DESKTOP;
|
||||
sid.accountid = $(users[1]).data('miniprofile');
|
||||
data.actor = sid;
|
||||
}
|
||||
|
||||
// Figure out the date. Of course there's no year, because Valve
|
||||
let dateParts = $item.find('.historyDate').text().split('@');
|
||||
let date = dateParts[0].trim().replace(/(st|nd|th)$/, '').trim() + ', ' + currentYear;
|
||||
let time = dateParts[1].trim().replace(/(am|pm)/, ' $1');
|
||||
|
||||
date = new Date(date + ' ' + time + ' UTC');
|
||||
|
||||
// If this date is in the future, or it's later than the previous one, decrement the year
|
||||
if (date.getTime() > lastDate) {
|
||||
date.setFullYear(date.getFullYear() - 1);
|
||||
}
|
||||
|
||||
data.date = date;
|
||||
|
||||
output.items.push(data);
|
||||
});
|
||||
|
||||
callback(null, output);
|
||||
}, 'steamcommunity');
|
||||
};
|
||||
|
||||
SteamCommunity.prototype.getAllGroupComments = function(gid, from, count, callback) {
|
||||
if (typeof gid == 'string') {
|
||||
gid = new SteamID(gid);
|
||||
}
|
||||
|
||||
let options = {
|
||||
url: `https://steamcommunity.com/comment/Clan/render/${gid.getSteamID64()}/-1/`,
|
||||
form: {
|
||||
start: from,
|
||||
count
|
||||
}
|
||||
};
|
||||
|
||||
this.httpRequestPost(options, (err, response, body) => {
|
||||
if (err) {
|
||||
callback(err);
|
||||
return;
|
||||
}
|
||||
|
||||
let comments = [];
|
||||
|
||||
let $ = Cheerio.load(JSON.parse(body).comments_html);
|
||||
|
||||
$('.commentthread_comment_content').each((i, element) => {
|
||||
let comment = {};
|
||||
|
||||
let $element = $(element);
|
||||
let $selector = $($element.find('.commentthread_author_link'));
|
||||
comment.authorName = $selector.find('bdi').text();
|
||||
comment.authorId = $selector.attr('href').replace(/https?:\/\/steamcommunity.com\/(id|profiles)\//, '');
|
||||
comment.date = Helpers.decodeSteamTime($(this).find('.commentthread_comment_timestamp').text().trim());
|
||||
|
||||
$selector = $($element.find('.commentthread_comment_text'));
|
||||
comment.commentId = $selector.attr('id').replace('comment_content_', '');
|
||||
comment.text = $selector.html().trim();
|
||||
|
||||
comments.push(comment);
|
||||
});
|
||||
|
||||
callback(null, comments);
|
||||
}, 'steamcommunity');
|
||||
};
|
||||
|
||||
SteamCommunity.prototype.deleteGroupComment = function(gid, cid, callback) {
|
||||
if (typeof gid == 'string') {
|
||||
gid = new SteamID(gid);
|
||||
}
|
||||
|
||||
if (typeof cid != 'string') {
|
||||
cid = cid.toString();
|
||||
}
|
||||
|
||||
let options = {
|
||||
url: `https://steamcommunity.com/comment/Clan/delete/${gid.getSteamID64()}/-1/`,
|
||||
form: {
|
||||
sessionid: this.getSessionID(),
|
||||
gidcomment: cid
|
||||
}
|
||||
};
|
||||
|
||||
this.httpRequestPost(options, (err, response, body) => {
|
||||
if (!callback) {
|
||||
return;
|
||||
}
|
||||
|
||||
callback(err || null);
|
||||
}, 'steamcommunity');
|
||||
};
|
||||
|
||||
SteamCommunity.prototype.postGroupComment = function(gid, message, callback) {
|
||||
if (typeof gid == 'string') {
|
||||
gid = new SteamID(gid);
|
||||
}
|
||||
|
||||
let options = {
|
||||
url: `https://steamcommunity.com/comment/Clan/post/${gid.getSteamID64()}/-1/`,
|
||||
form: {
|
||||
comment: message,
|
||||
count: 6,
|
||||
sessionid: this.getSessionID()
|
||||
}
|
||||
};
|
||||
|
||||
this.httpRequestPost(options, (err, response, body) => {
|
||||
if (!callback) {
|
||||
return;
|
||||
}
|
||||
|
||||
callback(err || null);
|
||||
}, 'steamcommunity');
|
||||
};
|
||||
|
||||
/**
|
||||
* Get requests to join a restricted group.
|
||||
* @param {SteamID|string} gid - The SteamID of the group you want to manage
|
||||
* @param {function} callback - First argument is null/Error, second is array of SteamID objects
|
||||
*/
|
||||
SteamCommunity.prototype.getGroupJoinRequests = function(gid, callback) {
|
||||
if (typeof gid == 'string') {
|
||||
gid = new SteamID(gid);
|
||||
}
|
||||
|
||||
this.httpRequestGet(`https://steamcommunity.com/gid/${gid.getSteamID64()}/joinRequestsManage`, (err, res, body) => {
|
||||
if (!body) {
|
||||
callback(new Error('Malformed response'));
|
||||
return;
|
||||
}
|
||||
|
||||
let matches = body.match(/JoinRequests_ApproveDenyUser\(\W*['"](\d+)['"],\W0\W\)/g);
|
||||
if (!matches) {
|
||||
// no pending requests
|
||||
callback(null, []);
|
||||
return;
|
||||
}
|
||||
|
||||
let requests = [];
|
||||
for (let i = 0; i < matches.length; i++) {
|
||||
requests.push(new SteamID('[U:1:' + matches[i].match(/JoinRequests_ApproveDenyUser\(\W*['"](\d+)['"],\W0\W\)/)[1] + ']'));
|
||||
}
|
||||
|
||||
callback(null, requests);
|
||||
}, 'steamcommunity');
|
||||
};
|
||||
|
||||
/**
|
||||
* Respond to one or more join requests to a restricted group.
|
||||
* @param {SteamID|string} gid - The SteamID of the group you want to manage
|
||||
* @param {SteamID|string|SteamID[]|string[]} steamIDs - The SteamIDs of the users you want to approve or deny membership for (or a single value)
|
||||
* @param {boolean} approve - True to put them in the group, false to deny their membership
|
||||
* @param {function} callback - Takes only an Error object/null as the first argument
|
||||
*/
|
||||
SteamCommunity.prototype.respondToGroupJoinRequests = function(gid, steamIDs, approve, callback) {
|
||||
if (typeof gid == 'string') {
|
||||
gid = new SteamID(gid);
|
||||
}
|
||||
|
||||
let rgAccounts = (!Array.isArray(steamIDs) ? [steamIDs] : steamIDs).map(sid => sid.toString());
|
||||
|
||||
this.httpRequestPost({
|
||||
url: `https://steamcommunity.com/gid/${gid.getSteamID64()}/joinRequestsManage`,
|
||||
form: {
|
||||
rgAccounts: rgAccounts,
|
||||
bapprove: approve ? '1' : '0',
|
||||
json: '1',
|
||||
sessionID: this.getSessionID()
|
||||
},
|
||||
json: true
|
||||
}, (err, res, body) => {
|
||||
if (!callback) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (body != EResult.OK) {
|
||||
let err = new Error(EResult[body] || `Error ${body}`);
|
||||
err.eresult = body;
|
||||
callback(err);
|
||||
} else {
|
||||
callback(null);
|
||||
}
|
||||
}, 'steamcommunity');
|
||||
};
|
||||
|
||||
/**
|
||||
* Respond to *ALL* pending group-join requests for a particular group.
|
||||
* @param {SteamID|string} gid - The SteamID of the group you want to manage
|
||||
* @param {boolean} approve - True to allow everyone who requested into the group, false to not
|
||||
* @param {function} callback - Takes only an Error object/null as the first argument
|
||||
*/
|
||||
SteamCommunity.prototype.respondToAllGroupJoinRequests = function(gid, approve, callback) {
|
||||
if (typeof gid == 'string') {
|
||||
gid = new SteamID(gid);
|
||||
}
|
||||
|
||||
this.httpRequestPost({
|
||||
url: `https://steamcommunity.com/gid/${gid.getSteamID64()}/joinRequestsManage`,
|
||||
form: {
|
||||
bapprove: approve ? '1' : '0',
|
||||
json: '1',
|
||||
action: 'bulkrespond',
|
||||
sessionID: this.getSessionID()
|
||||
},
|
||||
json: true
|
||||
}, (err, res, body) => {
|
||||
if (!callback) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (body != EResult.OK) {
|
||||
let err = new Error(EResult[body] || `Error ${body}`);
|
||||
err.eresult = body;
|
||||
callback(err);
|
||||
} else {
|
||||
callback(null);
|
||||
}
|
||||
}, 'steamcommunity');
|
||||
};
|
||||
66
components/help.js
Normal file
66
components/help.js
Normal file
@@ -0,0 +1,66 @@
|
||||
const StdLib = require('@doctormckay/stdlib');
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
const {HttpResponse} = require('@doctormckay/stdlib/http');
|
||||
|
||||
const SteamCommunity = require('../index.js');
|
||||
|
||||
const HELP_SITE_DOMAIN = 'https://help.steampowered.com';
|
||||
|
||||
/**
|
||||
* Restore a previously removed steam package from your steam account.
|
||||
* @param {int|string} packageID
|
||||
* @param {function} [callback]
|
||||
* @return Promise<void>
|
||||
*/
|
||||
SteamCommunity.prototype.restorePackage = function(packageID, callback) {
|
||||
return StdLib.Promises.callbackPromise(null, callback, true, async (resolve, reject) => {
|
||||
let result = await this.httpRequest({
|
||||
method: 'POST',
|
||||
url: `${HELP_SITE_DOMAIN}/wizard/AjaxDoPackageRestore`,
|
||||
form: {
|
||||
packageid: packageID,
|
||||
sessionid: this.getSessionID(HELP_SITE_DOMAIN),
|
||||
wizard_ajax: 1
|
||||
},
|
||||
source: 'steamcommunity'
|
||||
});
|
||||
|
||||
wizardAjaxHandler(result, resolve, reject);
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Remove a steam package from your steam account.
|
||||
* @param {int|string} packageID
|
||||
* @param {function} callback
|
||||
*/
|
||||
SteamCommunity.prototype.removePackage = function(packageID, callback) {
|
||||
return StdLib.Promises.callbackPromise(null, callback, true, async (resolve, reject) => {
|
||||
let result = await this.httpRequest({
|
||||
method: 'POST',
|
||||
url: `${HELP_SITE_DOMAIN}/wizard/AjaxDoPackageRemove`,
|
||||
form: {
|
||||
packageid: packageID,
|
||||
sessionid: this.getSessionID(HELP_SITE_DOMAIN),
|
||||
wizard_ajax: 1
|
||||
},
|
||||
source: 'steamcommunity'
|
||||
});
|
||||
|
||||
wizardAjaxHandler(result, resolve, reject);
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {HttpResponse} result
|
||||
* @param {function} resolve
|
||||
* @param {function} reject
|
||||
*/
|
||||
function wizardAjaxHandler(result, resolve, reject) {
|
||||
if (!result.jsonBody || !result.jsonBody.success) {
|
||||
return reject(new Error((result.jsonBody || {}).errorMsg || 'Unexpected error'));
|
||||
}
|
||||
|
||||
resolve();
|
||||
}
|
||||
64
components/helpers.js
Normal file
64
components/helpers.js
Normal file
@@ -0,0 +1,64 @@
|
||||
const EResult = require('../resources/EResult.js');
|
||||
|
||||
/**
|
||||
* Make sure that a provided input is a valid SteamID object.
|
||||
* @param {object} input
|
||||
* @returns {boolean}
|
||||
*/
|
||||
exports.isSteamID = function(input) {
|
||||
return ['universe', 'type', 'instance', 'accountid'].every(prop => typeof input[prop] == 'number' || typeof input[prop] == 'bigint');
|
||||
};
|
||||
|
||||
exports.decodeSteamTime = function(time) {
|
||||
let date = new Date();
|
||||
|
||||
if (time.includes('@')) {
|
||||
let parts = time.split('@');
|
||||
if (!parts[0].includes(',')) {
|
||||
// no year, assume current year
|
||||
parts[0] += ', ' + date.getFullYear();
|
||||
}
|
||||
|
||||
date = new Date(parts.join('@').replace(/(am|pm)/, ' $1') + ' UTC'); // add a space so JS can decode it
|
||||
} else {
|
||||
// Relative date
|
||||
let amount = time.replace(/(\d) (minutes|hour|hours) ago/, '$1');
|
||||
|
||||
if (time.includes('minutes')) {
|
||||
date.setMinutes(date.getMinutes() - amount);
|
||||
} else if (time.match(/hour|hours/)) {
|
||||
date.setHours(date.getHours() - amount);
|
||||
}
|
||||
}
|
||||
|
||||
return date;
|
||||
};
|
||||
|
||||
/**
|
||||
* Get an Error object for a particular EResult
|
||||
* @param {int|EResult} eresult
|
||||
* @param {string} [message] - If eresult is a failure code and message exists, this message will be used in the Error object instead
|
||||
* @returns {null|Error}
|
||||
*/
|
||||
exports.eresultError = function(eresult, message) {
|
||||
if (eresult == EResult.OK) {
|
||||
// no error
|
||||
return null;
|
||||
}
|
||||
|
||||
let err = new Error(message || EResult[eresult] || `Error ${eresult}`);
|
||||
err.eresult = eresult;
|
||||
return err;
|
||||
};
|
||||
|
||||
exports.decodeJwt = function(jwt) {
|
||||
let parts = jwt.split('.');
|
||||
if (parts.length != 3) {
|
||||
throw new Error('Invalid JWT');
|
||||
}
|
||||
|
||||
let standardBase64 = parts[1].replace(/-/g, '+')
|
||||
.replace(/_/g, '/');
|
||||
|
||||
return JSON.parse(Buffer.from(standardBase64, 'base64').toString('utf8'));
|
||||
};
|
||||
189
components/http.js
Normal file
189
components/http.js
Normal file
@@ -0,0 +1,189 @@
|
||||
const {HttpResponse} = require('@doctormckay/stdlib/http'); // eslint-disable-line
|
||||
const {betterPromise} = require('@doctormckay/stdlib/promises');
|
||||
|
||||
const SteamCommunity = require('../index.js');
|
||||
|
||||
/**
|
||||
* @param {object} options
|
||||
* @param {string} options.method
|
||||
* @param {string} options.url
|
||||
* @param {string} [options.source='']
|
||||
* @param {object} [options.qs]
|
||||
* @param {*} [options.body]
|
||||
* @param {object} [options.form]
|
||||
* @param {object} [options.multipartForm]
|
||||
* @param {boolean} [options.json=false] - Controls whether the *REQUEST* should be sent as json.
|
||||
* @param {boolean} [options.followRedirect=true]
|
||||
* @param {boolean} [options.checkHttpError=true]
|
||||
* @param {boolean} [options.checkCommunityError=true]
|
||||
* @param {boolean} [options.checkTradeError=true]
|
||||
* @param {boolean} [options.checkJsonError=true]
|
||||
* @return {Promise<HttpResponse>}
|
||||
*/
|
||||
SteamCommunity.prototype.httpRequest = function(options) {
|
||||
return betterPromise(async (resolve, reject) => {
|
||||
let requestID = ++this._httpRequestID;
|
||||
let source = options.source || '';
|
||||
|
||||
await betterPromise((resolve, reject) => {
|
||||
if (!this.onPreHttpRequest || !this.onPreHttpRequest(requestID, source, options, (err) => {
|
||||
err ? reject(err) : resolve();
|
||||
})) {
|
||||
// No pre-hook, or the pre-hook doesn't want to delay the request.
|
||||
resolve();
|
||||
}
|
||||
});
|
||||
|
||||
let result = await this._httpClient.request({
|
||||
method: options.method,
|
||||
url: options.url,
|
||||
queryString: options.qs,
|
||||
headers: options.headers,
|
||||
body: options.body,
|
||||
urlEncodedForm: options.form,
|
||||
multipartForm: options.multipartForm,
|
||||
json: options.json,
|
||||
followRedirects: options.followRedirect
|
||||
});
|
||||
|
||||
let httpError = options.checkHttpError !== false && this._checkHttpError(result);
|
||||
let communityError = !options.json && options.checkCommunityError !== false && this._checkCommunityError(result);
|
||||
let tradeError = !options.json && options.checkTradeError !== false && this._checkTradeError(result);
|
||||
let jsonError = options.json && options.checkJsonError !== false && !result.jsonBody ? new Error('Malformed JSON response') : null;
|
||||
|
||||
this.emit('postHttpRequest', {
|
||||
requestID,
|
||||
source,
|
||||
options,
|
||||
response: result,
|
||||
body: result.textBody,
|
||||
error: httpError || communityError || tradeError || jsonError || null,
|
||||
httpError,
|
||||
communityError,
|
||||
tradeError,
|
||||
jsonError
|
||||
});
|
||||
|
||||
resolve(result);
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* @param {string|object} endpoint
|
||||
* @param {object} [form]
|
||||
* @private
|
||||
*/
|
||||
SteamCommunity.prototype._myProfile = async function(endpoint, form) {
|
||||
if (!this._profileURL) {
|
||||
let result = await this.httpRequest({
|
||||
method: 'GET',
|
||||
url: 'https://steamcommunity.com/my',
|
||||
followRedirect: false,
|
||||
source: 'steamcommunity'
|
||||
});
|
||||
|
||||
if (result.statusCode != 302) {
|
||||
throw new Error(`HTTP error ${result.statusCode}`);
|
||||
}
|
||||
|
||||
let match = result.headers.location.match(/steamcommunity\.com(\/(id|profiles)\/[^/]+)\/?/);
|
||||
if (!match) {
|
||||
throw new Error('Can\'t get profile URL');
|
||||
}
|
||||
|
||||
this._profileURL = match[1];
|
||||
setTimeout(() => {
|
||||
delete this._profileURL; // delete the cache
|
||||
}, 60000).unref();
|
||||
}
|
||||
|
||||
let options = endpoint.endpoint ? endpoint : {};
|
||||
options.url = `https://steamcommunity.com${this._profileURL}/${endpoint.endpoint || endpoint}`;
|
||||
options.followRedirect = true;
|
||||
|
||||
if (form) {
|
||||
options.method = 'POST';
|
||||
options.form = form;
|
||||
} else if (!options.method) {
|
||||
options.method = 'GET';
|
||||
}
|
||||
|
||||
options.source = 'steamcommunity';
|
||||
|
||||
return await this.httpRequest(options);
|
||||
};
|
||||
|
||||
SteamCommunity.prototype._notifySessionExpired = function(err) {
|
||||
this.emit('sessionExpired', err);
|
||||
};
|
||||
|
||||
/**
|
||||
* @param {HttpResponse} response
|
||||
* @return {Error|boolean}
|
||||
* @private
|
||||
*/
|
||||
SteamCommunity.prototype._checkHttpError = function(response) {
|
||||
if (response.statusCode >= 300 && response.statusCode <= 399 && response.headers.location.indexOf('/login') != -1) {
|
||||
let err = new Error('Not Logged In');
|
||||
this._notifySessionExpired(err);
|
||||
return err;
|
||||
}
|
||||
|
||||
if (
|
||||
response.statusCode == 403
|
||||
&& typeof response.textBody == 'string'
|
||||
&& response.textBody.match(/<div id="parental_notice_instructions">Enter your PIN below to exit Family View.<\/div>/)
|
||||
) {
|
||||
return new Error('Family View Restricted');
|
||||
}
|
||||
|
||||
if (response.statusCode >= 400) {
|
||||
let err = new Error(`HTTP error ${response.statusCode}`);
|
||||
err.code = response.statusCode;
|
||||
return err;
|
||||
}
|
||||
|
||||
return false;
|
||||
};
|
||||
|
||||
/**
|
||||
* @param {HttpResponse} response
|
||||
* @return {Error|boolean}
|
||||
* @private
|
||||
*/
|
||||
SteamCommunity.prototype._checkCommunityError = function(response) {
|
||||
let html = response.textBody;
|
||||
|
||||
if (typeof html == 'string' && html.match(/<h1>Sorry!<\/h1>/)) {
|
||||
let match = html.match(/<h3>(.+)<\/h3>/);
|
||||
return new Error(match ? match[1] : 'Unknown error occurred');
|
||||
}
|
||||
|
||||
if (typeof html == 'string' && html.indexOf('g_steamID = false;') > -1 && html.indexOf('<title>Sign In</title>') > -1) {
|
||||
let err = new Error('Not Logged In');
|
||||
this._notifySessionExpired(err);
|
||||
return err;
|
||||
}
|
||||
|
||||
return false;
|
||||
};
|
||||
|
||||
/**
|
||||
* @param {HttpResponse} response
|
||||
* @return {Error|boolean}
|
||||
* @private
|
||||
*/
|
||||
SteamCommunity.prototype._checkTradeError = function(response) {
|
||||
let html = response.textBody;
|
||||
|
||||
if (typeof html !== 'string') {
|
||||
return false;
|
||||
}
|
||||
|
||||
let match = html.match(/<div id="error_msg">\s*([^<]+)\s*<\/div>/);
|
||||
if (match) {
|
||||
return new Error(match[1].trim());
|
||||
}
|
||||
|
||||
return false;
|
||||
};
|
||||
376
components/market.js
Normal file
376
components/market.js
Normal file
@@ -0,0 +1,376 @@
|
||||
const Cheerio = require('cheerio');
|
||||
|
||||
const SteamCommunity = require('../index.js');
|
||||
const Helpers = require('./helpers.js');
|
||||
|
||||
/**
|
||||
* Get a list of all apps on the market
|
||||
* @param {function} callback - First argument is null|Error, second is an object of appid => name
|
||||
*/
|
||||
SteamCommunity.prototype.getMarketApps = function(callback) {
|
||||
this.httpRequest('https://steamcommunity.com/market/', (err, response, body) => {
|
||||
if (err) {
|
||||
callback(err);
|
||||
return;
|
||||
}
|
||||
|
||||
let $ = Cheerio.load(body);
|
||||
if ($('.market_search_game_button_group')) {
|
||||
let apps = {};
|
||||
$('.market_search_game_button_group a.game_button').each((i, element) => {
|
||||
let e = Cheerio.load(element);
|
||||
let name = e('.game_button_game_name').text().trim();
|
||||
let url = element.attribs.href;
|
||||
let appid = url.substr(url.indexOf('=') + 1);
|
||||
apps[appid] = name;
|
||||
});
|
||||
callback(null, apps);
|
||||
} else {
|
||||
callback(new Error('Malformed response'));
|
||||
}
|
||||
}, 'steamcommunity');
|
||||
};
|
||||
|
||||
/**
|
||||
* Check if an item is eligible to be turned into gems and if so, get its gem value
|
||||
* @param {int} appid
|
||||
* @param {int|string} assetid
|
||||
* @param {function} callback
|
||||
*/
|
||||
SteamCommunity.prototype.getGemValue = function(appid, assetid, callback) {
|
||||
this._myProfile({
|
||||
endpoint: 'ajaxgetgoovalue/',
|
||||
qs: {
|
||||
sessionid: this.getSessionID(),
|
||||
appid: appid,
|
||||
contextid: 6,
|
||||
assetid: assetid
|
||||
},
|
||||
checkHttpError: false,
|
||||
json: true
|
||||
}, null, (err, res, body) => {
|
||||
if (err) {
|
||||
callback(err);
|
||||
return;
|
||||
}
|
||||
|
||||
let err2 = Helpers.eresultError(body.success, body.message);
|
||||
if (err2) {
|
||||
return callback(err2);
|
||||
}
|
||||
|
||||
if (!body.goo_value || !body.strTitle) {
|
||||
callback(new Error('Malformed response'));
|
||||
return;
|
||||
}
|
||||
|
||||
callback(null, {promptTitle: body.strTitle, gemValue: parseInt(body.goo_value, 10)});
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Turn an eligible item into gems.
|
||||
* @param {int} appid
|
||||
* @param {int|string} assetid
|
||||
* @param {int} expectedGemsValue
|
||||
* @param {function} callback
|
||||
*/
|
||||
SteamCommunity.prototype.turnItemIntoGems = function(appid, assetid, expectedGemsValue, callback) {
|
||||
this._myProfile({
|
||||
endpoint: 'ajaxgrindintogoo/',
|
||||
json: true,
|
||||
checkHttpError: false
|
||||
}, {
|
||||
appid: appid,
|
||||
contextid: 6,
|
||||
assetid: assetid,
|
||||
goo_value_expected: expectedGemsValue,
|
||||
sessionid: this.getSessionID()
|
||||
}, (err, res, body) => {
|
||||
if (err) {
|
||||
callback(err);
|
||||
return;
|
||||
}
|
||||
|
||||
let err2 = Helpers.eresultError(body.success, body.message);
|
||||
if (err2) {
|
||||
return callback(err2);
|
||||
}
|
||||
|
||||
if (!body['goo_value_received '] || !body.goo_value_total) { // lol valve, that trailing space is real
|
||||
callback(new Error('Malformed response'));
|
||||
return;
|
||||
}
|
||||
|
||||
callback(null, {gemsReceived: parseInt(body['goo_value_received '], 10), totalGems: parseInt(body.goo_value_total, 10)});
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Open a booster pack.
|
||||
* @param {int} appid
|
||||
* @param {int|string} assetid
|
||||
* @param {function} callback
|
||||
*/
|
||||
SteamCommunity.prototype.openBoosterPack = function(appid, assetid, callback) {
|
||||
this._myProfile({
|
||||
endpoint: 'ajaxunpackbooster/',
|
||||
json: true,
|
||||
checkHttpError: false
|
||||
}, {
|
||||
appid: appid,
|
||||
communityitemid: assetid,
|
||||
sessionid: this.getSessionID()
|
||||
}, (err, res, body) => {
|
||||
if (err) {
|
||||
callback(err);
|
||||
return;
|
||||
}
|
||||
|
||||
let err2 = Helpers.eresultError(body.success, body.message);
|
||||
if (err2) {
|
||||
return callback(err2);
|
||||
}
|
||||
|
||||
if (!body.rgItems) {
|
||||
callback(new Error('Malformed response'));
|
||||
return;
|
||||
}
|
||||
|
||||
callback(null, body.rgItems);
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Get the booster pack catalog to see what booster packs you can create
|
||||
* @param {function} callback
|
||||
*/
|
||||
SteamCommunity.prototype.getBoosterPackCatalog = function(callback) {
|
||||
this.httpRequestGet('https://steamcommunity.com/tradingcards/boostercreator/', (err, res, body) => {
|
||||
if (err) {
|
||||
callback(err);
|
||||
return;
|
||||
}
|
||||
|
||||
let idx = body.indexOf('CBoosterCreatorPage.Init(');
|
||||
if (idx == -1) {
|
||||
callback(new Error('Malformed response'));
|
||||
return;
|
||||
}
|
||||
|
||||
let lines = body.slice(idx).split('\n').map(l => l.trim());
|
||||
|
||||
for (let i = 1; i <= 4; i++) {
|
||||
if (typeof lines[i] != 'string' || !lines[i].match(/,$/)) {
|
||||
let err = new Error('Malformed response');
|
||||
err.line = i;
|
||||
callback(err);
|
||||
return;
|
||||
}
|
||||
|
||||
lines[i] = lines[i].replace(/,$/, '');
|
||||
}
|
||||
|
||||
let boosterPackCatalog, totalGems, tradableGems, untradableGems;
|
||||
try {
|
||||
boosterPackCatalog = JSON.parse(lines[1]);
|
||||
totalGems = parseInt(lines[2].match(/\d+/)[0], 10);
|
||||
tradableGems = parseInt(lines[3].match(/\d+/)[0], 10);
|
||||
untradableGems = parseInt(lines[4].match(/\d+/)[0], 10);
|
||||
} catch (ex) {
|
||||
let err = new Error('Malformed response');
|
||||
err.inner = ex;
|
||||
callback(err);
|
||||
return;
|
||||
}
|
||||
|
||||
let keyedCatalog = {};
|
||||
boosterPackCatalog.forEach((app) => {
|
||||
app.price = parseInt(app.price, 10);
|
||||
app.unavailable = app.unavailable || false;
|
||||
app.availableAtTime = app.available_at_time || null;
|
||||
|
||||
if (typeof app.availableAtTime == 'string') {
|
||||
app.availableAtTime = Helpers.decodeSteamTime(app.availableAtTime);
|
||||
}
|
||||
|
||||
delete app.available_at_time;
|
||||
|
||||
keyedCatalog[app.appid] = app;
|
||||
});
|
||||
|
||||
callback(null, {
|
||||
totalGems,
|
||||
tradableGems,
|
||||
untradableGems,
|
||||
catalog: keyedCatalog
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Create a booster pack using gems.
|
||||
* @param {int} appid
|
||||
* @param {boolean} [useUntradableGems=false]
|
||||
* @param callback
|
||||
*/
|
||||
SteamCommunity.prototype.createBoosterPack = function(appid, useUntradableGems, callback) {
|
||||
if (typeof useUntradableGems == 'function') {
|
||||
callback = useUntradableGems;
|
||||
useUntradableGems = false;
|
||||
}
|
||||
|
||||
this.httpRequestPost({
|
||||
url: 'https://steamcommunity.com/tradingcards/ajaxcreatebooster/',
|
||||
form: {
|
||||
sessionid: this.getSessionID(),
|
||||
appid,
|
||||
series: 1,
|
||||
// tradability_preference can be a value 1-3
|
||||
// 1: Prefer using tradable gems, but use untradable if necessary
|
||||
// 2: Only use tradable gems
|
||||
// 3: Prefer using untradable gems, but use tradable if necessary
|
||||
tradability_preference: useUntradableGems ? 3 : 2
|
||||
},
|
||||
json: true,
|
||||
checkHttpError: false
|
||||
}, (err, res, body) => {
|
||||
if (err) {
|
||||
callback(err);
|
||||
return;
|
||||
}
|
||||
|
||||
if (body.purchase_eresult && body.purchase_eresult != 1) {
|
||||
callback(Helpers.eresultError(body.purchase_eresult));
|
||||
return;
|
||||
}
|
||||
|
||||
// We can now check HTTP status codes
|
||||
if (this._checkHttpError(err, res, callback, body)) {
|
||||
// TODO v4
|
||||
return;
|
||||
}
|
||||
|
||||
callback(null, {
|
||||
totalGems: parseInt(body.goo_amount, 10),
|
||||
tradableGems: parseInt(body.tradable_goo_amount, 10),
|
||||
untradableGems: parseInt(body.untradable_goo_amount, 10),
|
||||
resultItem: body.purchase_result
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Get details about a gift in your inventory.
|
||||
* @param {string} giftID
|
||||
* @param {function} callback
|
||||
*/
|
||||
SteamCommunity.prototype.getGiftDetails = function(giftID, callback) {
|
||||
this.httpRequestPost({
|
||||
url: `https://steamcommunity.com/gifts/${giftID}/validateunpack`,
|
||||
form: {
|
||||
sessionid: this.getSessionID()
|
||||
},
|
||||
json: true
|
||||
}, (err, res, body) => {
|
||||
if (err) {
|
||||
callback(err);
|
||||
return;
|
||||
}
|
||||
|
||||
let err2 = Helpers.eresultError(body.success, body.message);
|
||||
if (err2) {
|
||||
return callback(err2);
|
||||
}
|
||||
|
||||
if (!body.packageid || !body.gift_name) {
|
||||
callback(new Error('Malformed response'));
|
||||
return;
|
||||
}
|
||||
|
||||
callback(null, {
|
||||
giftName: body.gift_name,
|
||||
packageID: parseInt(body.packageid, 10),
|
||||
owned: body.owned
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Unpack a gift in your inventory to your library.
|
||||
* @param {string} giftID
|
||||
* @param {function} callback
|
||||
*/
|
||||
SteamCommunity.prototype.redeemGift = function(giftID, callback) {
|
||||
this.httpRequestPost({
|
||||
url: `https://steamcommunity.com/gifts/${giftID}/unpack`,
|
||||
form: {
|
||||
sessionid: this.getSessionID()
|
||||
},
|
||||
json: true
|
||||
}, (err, res, body) => {
|
||||
if (err) {
|
||||
callback(err);
|
||||
return;
|
||||
}
|
||||
|
||||
let err2 = Helpers.eresultError(body.success, body.message);
|
||||
if (err2) {
|
||||
return callback(err2);
|
||||
}
|
||||
|
||||
callback(null);
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* @param {int|string} assetid
|
||||
* @param {int} denominationIn
|
||||
* @param {int} denominationOut
|
||||
* @param {int} quantityIn
|
||||
* @param {int} quantityOut
|
||||
* @param {function} callback
|
||||
* @private
|
||||
*/
|
||||
SteamCommunity.prototype._gemExchange = function(assetid, denominationIn, denominationOut, quantityIn, quantityOut, callback) {
|
||||
this._myProfile({
|
||||
endpoint: 'ajaxexchangegoo/',
|
||||
json: true,
|
||||
checkHttpError: false
|
||||
}, {
|
||||
appid: 753,
|
||||
assetid,
|
||||
goo_denomination_in: denominationIn,
|
||||
goo_amount_in: quantityIn,
|
||||
goo_denomination_out: denominationOut,
|
||||
goo_amount_out_expected: quantityOut,
|
||||
sessionid: this.getSessionID()
|
||||
}, (err, res, body) => {
|
||||
if (err) {
|
||||
callback(err);
|
||||
return;
|
||||
}
|
||||
|
||||
callback(Helpers.eresultError(body.success));
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Pack gems into sack of gems.
|
||||
* @param {int|string} assetid - ID of gem stack you want to pack into sacks
|
||||
* @param {int} desiredSackCount - How many sacks you want. You must have at least this amount * 1000 gems in the stack you're packing
|
||||
* @param {function} callback
|
||||
*/
|
||||
SteamCommunity.prototype.packGemSacks = function(assetid, desiredSackCount, callback) {
|
||||
this._gemExchange(assetid, 1, 1000, desiredSackCount * 1000, desiredSackCount, callback);
|
||||
};
|
||||
|
||||
/**
|
||||
* Unpack sack of gems into gems.
|
||||
* @param {int|string} assetid - ID of sack stack you want to unpack (say that 5 times fast)
|
||||
* @param {int} sacksToUnpack
|
||||
* @param {function} callback
|
||||
*/
|
||||
SteamCommunity.prototype.unpackGemSacks = function(assetid, sacksToUnpack, callback) {
|
||||
this._gemExchange(assetid, 1000, 1, sacksToUnpack, sacksToUnpack * 1000, callback);
|
||||
};
|
||||
455
components/profile.js
Normal file
455
components/profile.js
Normal file
@@ -0,0 +1,455 @@
|
||||
const Cheerio = require('cheerio');
|
||||
const FS = require('fs');
|
||||
|
||||
const Helpers = require('./helpers.js');
|
||||
const SteamCommunity = require('../index.js');
|
||||
|
||||
SteamCommunity.PrivacyState = {
|
||||
Private: 1,
|
||||
FriendsOnly: 2,
|
||||
Public: 3
|
||||
};
|
||||
|
||||
const CommentPrivacyState = {
|
||||
1: 2, // private
|
||||
2: 0, // friends only
|
||||
3: 1 // anyone
|
||||
};
|
||||
|
||||
/**
|
||||
* Creates a profile page if you don't already have one.
|
||||
* @param {function} callback
|
||||
*/
|
||||
SteamCommunity.prototype.setupProfile = function(callback) {
|
||||
this._myProfile('edit?welcomed=1', null, (err, response, body) => {
|
||||
if (!callback) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (err || response.statusCode != 200) {
|
||||
callback(err || new Error('HTTP error ' + response.statusCode));
|
||||
} else {
|
||||
callback(null);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Edits your profile details.
|
||||
* @param {object} settings
|
||||
* @param {function} callback
|
||||
*/
|
||||
SteamCommunity.prototype.editProfile = function(settings, callback) {
|
||||
this._myProfile('edit/info', null, (err, response, body) => {
|
||||
if (err || response.statusCode != 200) {
|
||||
if (callback) {
|
||||
callback(err || new Error('HTTP error ' + response.statusCode));
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
let $ = Cheerio.load(body);
|
||||
let existingSettings = $('#profile_edit_config').data('profile-edit');
|
||||
if (!existingSettings || !existingSettings.strPersonaName) {
|
||||
if (callback) {
|
||||
callback(new Error('Malformed response'));
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
let values = {
|
||||
sessionID: this.getSessionID(),
|
||||
type: 'profileSave',
|
||||
weblink_1_title: '',
|
||||
weblink_1_url: '',
|
||||
weblink_2_title: '',
|
||||
weblink_2_url: '',
|
||||
weblink_3_title: '',
|
||||
weblink_3_url: '',
|
||||
personaName: existingSettings.strPersonaName,
|
||||
real_name: existingSettings.strRealName,
|
||||
summary: existingSettings.strSummary,
|
||||
country: existingSettings.LocationData.locCountryCode,
|
||||
state: existingSettings.LocationData.locStateCode,
|
||||
city: existingSettings.LocationData.locCityCode,
|
||||
customURL: existingSettings.strCustomURL,
|
||||
json: 1
|
||||
};
|
||||
|
||||
for (let i in settings) {
|
||||
switch (i) {
|
||||
case 'name':
|
||||
values.personaName = settings[i];
|
||||
break;
|
||||
|
||||
case 'realName':
|
||||
values.real_name = settings[i];
|
||||
break;
|
||||
|
||||
case 'summary':
|
||||
values.summary = settings[i];
|
||||
break;
|
||||
|
||||
case 'country':
|
||||
values.country = settings[i];
|
||||
break;
|
||||
|
||||
case 'state':
|
||||
values.state = settings[i];
|
||||
break;
|
||||
|
||||
case 'city':
|
||||
values.city = settings[i];
|
||||
break;
|
||||
|
||||
case 'customURL':
|
||||
values.customURL = settings[i];
|
||||
break;
|
||||
|
||||
case 'primaryGroup':
|
||||
if(typeof settings[i] === 'object' && settings[i].getSteamID64) {
|
||||
values.primary_group_steamid = settings[i].getSteamID64();
|
||||
} else {
|
||||
values.primary_group_steamid = new SteamID(settings[i]).getSteamID64();
|
||||
}
|
||||
|
||||
break;
|
||||
|
||||
// These don't work right now
|
||||
/*
|
||||
case 'background':
|
||||
// The assetid of our desired profile background
|
||||
values.profile_background = settings[i];
|
||||
break;
|
||||
|
||||
case 'featuredBadge':
|
||||
// Currently, game badges aren't supported
|
||||
values.favorite_badge_badgeid = settings[i];
|
||||
break;
|
||||
*/
|
||||
// TODO: profile showcases
|
||||
}
|
||||
}
|
||||
|
||||
this._myProfile('edit', values, (err, response, body) => {
|
||||
if (settings.customURL) {
|
||||
delete this._profileURL;
|
||||
}
|
||||
|
||||
if (!callback) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (err || response.statusCode != 200) {
|
||||
callback(err || new Error('HTTP error ' + response.statusCode));
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
let json = JSON.parse(body);
|
||||
let err2 = Helpers.eresultError(json.success, json.errmsg);
|
||||
if (err2) {
|
||||
return callback(err2);
|
||||
}
|
||||
|
||||
callback(null);
|
||||
} catch (ex) {
|
||||
callback(ex);
|
||||
}
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
SteamCommunity.prototype.profileSettings = function(settings, callback) {
|
||||
this._myProfile('edit/settings', null, (err, response, body) => {
|
||||
if (err || response.statusCode != 200) {
|
||||
if (callback) {
|
||||
callback(err || new Error('HTTP error ' + response.statusCode));
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
let $ = Cheerio.load(body);
|
||||
let existingSettings = $('#profile_edit_config').data('profile-edit');
|
||||
if (!existingSettings || !existingSettings.Privacy) {
|
||||
if (callback) {
|
||||
callback(new Error('Malformed response'));
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
// PrivacySettings => {PrivacyProfile, PrivacyInventory, PrivacyInventoryGifts, PrivacyOwnedGames, PrivacyPlaytime}
|
||||
// eCommentPermission
|
||||
let privacy = existingSettings.Privacy.PrivacySettings;
|
||||
let commentPermission = existingSettings.Privacy.eCommentPermission;
|
||||
|
||||
for (let i in settings) {
|
||||
switch (i) {
|
||||
case 'profile':
|
||||
privacy.PrivacyProfile = settings[i];
|
||||
break;
|
||||
|
||||
case 'comments':
|
||||
commentPermission = CommentPrivacyState[settings[i]];
|
||||
break;
|
||||
|
||||
case 'inventory':
|
||||
privacy.PrivacyInventory = settings[i];
|
||||
break;
|
||||
|
||||
case 'inventoryGifts':
|
||||
privacy.PrivacyInventoryGifts = settings[i] ? SteamCommunity.PrivacyState.Private : SteamCommunity.PrivacyState.Public;
|
||||
break;
|
||||
|
||||
case 'gameDetails':
|
||||
privacy.PrivacyOwnedGames = settings[i];
|
||||
break;
|
||||
|
||||
case 'playtime':
|
||||
privacy.PrivacyPlaytime = settings[i] ? SteamCommunity.PrivacyState.Private : SteamCommunity.PrivacyState.Public;
|
||||
break;
|
||||
|
||||
case 'friendsList':
|
||||
privacy.PrivacyFriendsList = settings[i];
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
this._myProfile({
|
||||
method: 'POST',
|
||||
endpoint: 'ajaxsetprivacy/',
|
||||
json: true,
|
||||
formData: { // it's multipart because lolvalve
|
||||
sessionid: this.getSessionID(),
|
||||
Privacy: JSON.stringify(privacy),
|
||||
eCommentPermission: commentPermission
|
||||
}
|
||||
}, null, (err, response, body) => {
|
||||
if (err || response.statusCode != 200) {
|
||||
if (callback) {
|
||||
callback(err || new Error('HTTP error ' + response.statusCode));
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
let err2 = Helpers.eresultError(body.success);
|
||||
if (err2) {
|
||||
callback && callback(err2);
|
||||
return;
|
||||
}
|
||||
|
||||
if (callback) {
|
||||
callback(null, body.Privacy);
|
||||
}
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
SteamCommunity.prototype.uploadAvatar = function(image, format, callback) {
|
||||
if (typeof format === 'function') {
|
||||
callback = format;
|
||||
format = null;
|
||||
}
|
||||
|
||||
// are we logged in?
|
||||
if (!this.steamID) {
|
||||
callback(new Error('Not Logged In'));
|
||||
return;
|
||||
}
|
||||
|
||||
const doUpload = (buffer) => {
|
||||
if (!format) {
|
||||
if (callback) {
|
||||
callback(new Error('Unknown image format'));
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if (format.match(/^image\//)) {
|
||||
format = format.substring(6);
|
||||
}
|
||||
|
||||
let filename = '';
|
||||
let contentType = '';
|
||||
|
||||
switch (format.toLowerCase()) {
|
||||
case 'jpg':
|
||||
case 'jpeg':
|
||||
filename = 'avatar.jpg';
|
||||
contentType = 'image/jpeg';
|
||||
break;
|
||||
|
||||
case 'png':
|
||||
filename = 'avatar.png';
|
||||
contentType = 'image/png';
|
||||
break;
|
||||
|
||||
case 'gif':
|
||||
filename = 'avatar.gif';
|
||||
contentType = 'image/gif';
|
||||
break;
|
||||
|
||||
default:
|
||||
if (callback) {
|
||||
callback(new Error('Unknown or invalid image format'));
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
this.httpRequestPost({
|
||||
url: 'https://steamcommunity.com/actions/FileUploader',
|
||||
formData: {
|
||||
MAX_FILE_SIZE: buffer.length,
|
||||
type: 'player_avatar_image',
|
||||
sId: this.steamID.getSteamID64(),
|
||||
sessionid: this.getSessionID(),
|
||||
doSub: 1,
|
||||
json: 1,
|
||||
avatar: {
|
||||
value: buffer,
|
||||
options: {
|
||||
filename: filename,
|
||||
contentType: contentType
|
||||
}
|
||||
}
|
||||
},
|
||||
json: true
|
||||
}, (err, response, body) => {
|
||||
if (err) {
|
||||
callback && callback(err);
|
||||
return;
|
||||
}
|
||||
|
||||
if (body && !body.success && body.message) {
|
||||
callback && callback(new Error(body.message));
|
||||
return;
|
||||
}
|
||||
|
||||
if (response.statusCode != 200) {
|
||||
callback && callback(new Error(`HTTP error ${response.statusCode}`));
|
||||
return;
|
||||
}
|
||||
|
||||
if (!body || !body.success) {
|
||||
callback && callback(new Error('Malformed response'));
|
||||
return;
|
||||
}
|
||||
|
||||
callback && callback(null, body.images.full);
|
||||
}, 'steamcommunity');
|
||||
};
|
||||
|
||||
if (image instanceof Buffer) {
|
||||
doUpload(image);
|
||||
} else if (image.match(/^https?:\/\//)) {
|
||||
this.httpRequestGet({
|
||||
url: image,
|
||||
encoding: null
|
||||
}, (err, response, body) => {
|
||||
if (err || response.statusCode != 200) {
|
||||
if (callback) {
|
||||
callback(new Error(err ? `${err.message} downloading image` : `HTTP error ${response.statusCode} downloading image`));
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if (!format) {
|
||||
format = response.headers['content-type'];
|
||||
}
|
||||
|
||||
doUpload(body);
|
||||
}, 'steamcommunity');
|
||||
} else {
|
||||
if (!format) {
|
||||
format = image.match(/\.([^.]+)$/);
|
||||
if (format) {
|
||||
format = format[1];
|
||||
}
|
||||
}
|
||||
|
||||
FS.readFile(image, (err, file) => {
|
||||
if (err) {
|
||||
if (callback) {
|
||||
callback(err);
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
doUpload(file);
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Post a new status to your profile activity feed.
|
||||
* @param {string} statusText - The text of this status update
|
||||
* @param {{appID: int}} [options] - Options for this status update. All are optional. If you don't pass any options, this can be omitted.
|
||||
* @param {function} callback - err, postID
|
||||
*/
|
||||
SteamCommunity.prototype.postProfileStatus = function(statusText, options, callback) {
|
||||
if (typeof options === 'function') {
|
||||
callback = options;
|
||||
options = {};
|
||||
}
|
||||
|
||||
this._myProfile('ajaxpostuserstatus/', {
|
||||
appid: options.appID || 0,
|
||||
sessionid: this.getSessionID(),
|
||||
status_text: statusText
|
||||
}, (err, res, body) => {
|
||||
try {
|
||||
body = JSON.parse(body);
|
||||
if (body.message) {
|
||||
callback(new Error(body.message));
|
||||
return;
|
||||
}
|
||||
|
||||
let match = body.blotter_html.match(/id="userstatus_(\d+)_/);
|
||||
if (!match) {
|
||||
callback(new Error('Malformed response'));
|
||||
return;
|
||||
}
|
||||
|
||||
callback(null, parseInt(match[1], 10));
|
||||
} catch (ex) {
|
||||
callback(ex);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Delete a previously-posted profile status update.
|
||||
* @param {int} postID
|
||||
* @param {function} [callback]
|
||||
*/
|
||||
SteamCommunity.prototype.deleteProfileStatus = function(postID, callback) {
|
||||
this._myProfile('ajaxdeleteuserstatus/', {
|
||||
sessionid: this.getSessionID(),
|
||||
postid: postID
|
||||
}, (err, res, body) => {
|
||||
if (!callback) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
body = JSON.parse(body);
|
||||
if (!body.success) {
|
||||
callback(new Error('Malformed response'));
|
||||
return;
|
||||
}
|
||||
|
||||
callback(Helpers.eresultError(body.success));
|
||||
} catch (ex) {
|
||||
callback(ex);
|
||||
}
|
||||
});
|
||||
};
|
||||
163
components/sharedfiles.js
Normal file
163
components/sharedfiles.js
Normal file
@@ -0,0 +1,163 @@
|
||||
const StdLib = require('@doctormckay/stdlib');
|
||||
const SteamID = require('steamid');
|
||||
|
||||
const SteamCommunity = require('../index.js');
|
||||
|
||||
|
||||
/**
|
||||
* Deletes a comment from a sharedfile's comment section
|
||||
* @param {SteamID | String} userID - ID of the user associated to this sharedfile
|
||||
* @param {String} sharedFileId - ID of the sharedfile
|
||||
* @param {String} cid - ID of the comment to delete
|
||||
* @param {function} [callback] - Takes only an Error object/null as the first argument
|
||||
* @return Promise<void>
|
||||
*/
|
||||
SteamCommunity.prototype.deleteSharedFileComment = function(userID, sharedFileId, cid, callback) {
|
||||
if (typeof userID == 'string') {
|
||||
userID = new SteamID(userID);
|
||||
}
|
||||
|
||||
return StdLib.Promises.callbackPromise(null, callback, true, async (resolve, reject) => {
|
||||
await this.httpRequest({
|
||||
method: 'POST',
|
||||
url: `https://steamcommunity.com/comment/PublishedFile_Public/delete/${userID.toString()}/${sharedFileId}/`,
|
||||
form: {
|
||||
gidcomment: cid,
|
||||
count: 10,
|
||||
sessionid: this.getSessionID()
|
||||
},
|
||||
source: 'steamcommunity'
|
||||
});
|
||||
|
||||
resolve();
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Favorites a sharedfile
|
||||
* @param {String} sharedFileId - ID of the sharedfile
|
||||
* @param {String} appid - ID of the app associated to this sharedfile
|
||||
* @param {function} [callback] - Takes only an Error object/null as the first argument
|
||||
* @return Promise<void>
|
||||
*/
|
||||
SteamCommunity.prototype.favoriteSharedFile = function(sharedFileId, appid, callback) {
|
||||
return StdLib.Promises.callbackPromise(null, callback, true, async (resolve, reject) => {
|
||||
await this.httpRequest({
|
||||
method: 'POST',
|
||||
url: 'https://steamcommunity.com/sharedfiles/favorite',
|
||||
form: {
|
||||
id: sharedFileId,
|
||||
appid,
|
||||
sessionid: this.getSessionID()
|
||||
},
|
||||
source: 'steamcommunity'
|
||||
});
|
||||
|
||||
resolve();
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Posts a comment to a sharedfile
|
||||
* @param {SteamID | String} userID - ID of the user associated to this sharedfile
|
||||
* @param {String} sharedFileId - ID of the sharedfile
|
||||
* @param {String} message - Content of the comment to post
|
||||
* @param {function} [callback] - Takes only an Error object/null as the first argument
|
||||
* @return Promise<void>
|
||||
*/
|
||||
SteamCommunity.prototype.postSharedFileComment = function(userID, sharedFileId, message, callback) {
|
||||
if (typeof userID == 'string') {
|
||||
userID = new SteamID(userID);
|
||||
}
|
||||
|
||||
return StdLib.Promises.callbackPromise(null, callback, true, async (resolve, reject) => {
|
||||
await this.httpRequest({
|
||||
method: 'POST',
|
||||
url: `https://steamcommunity.com/comment/PublishedFile_Public/post/${userID.toString()}/${sharedFileId}/`,
|
||||
form: {
|
||||
comment: message,
|
||||
count: 10,
|
||||
sessionid: this.getSessionID()
|
||||
},
|
||||
source: 'steamcommunity'
|
||||
});
|
||||
|
||||
resolve();
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Subscribes to a sharedfile's comment section. Note: Checkbox on webpage does not update
|
||||
* @param {SteamID | String} userID ID of the user associated to this sharedfile
|
||||
* @param {String} sharedFileId ID of the sharedfile
|
||||
* @param {function} [callback] - Takes only an Error object/null as the first argument
|
||||
* @return Promise<void>
|
||||
*/
|
||||
SteamCommunity.prototype.subscribeSharedFileComments = function(userID, sharedFileId, callback) {
|
||||
if (typeof userID == 'string') {
|
||||
userID = new SteamID(userID);
|
||||
}
|
||||
|
||||
return StdLib.Promises.callbackPromise(null, callback, true, async (resolve, reject) => {
|
||||
await this.httpRequest({
|
||||
method: 'POST',
|
||||
url: `https://steamcommunity.com/comment/PublishedFile_Public/subscribe/${userID.toString()}/${sharedFileId}/`,
|
||||
form: {
|
||||
count: 10,
|
||||
sessionid: this.getSessionID()
|
||||
},
|
||||
source: 'steamcommunity'
|
||||
});
|
||||
|
||||
resolve();
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Unfavorites a sharedfile
|
||||
* @param {String} sharedFileId - ID of the sharedfile
|
||||
* @param {String} appid - ID of the app associated to this sharedfile
|
||||
* @param {function} [callback] - Takes only an Error object/null as the first argument
|
||||
* @return Promise<void>
|
||||
*/
|
||||
SteamCommunity.prototype.unfavoriteSharedFile = function(sharedFileId, appid, callback) {
|
||||
return StdLib.Promises.callbackPromise(null, callback, true, async (resolve, reject) => {
|
||||
await this.httpRequest({
|
||||
method: 'POST',
|
||||
url: 'https://steamcommunity.com/sharedfiles/unfavorite',
|
||||
form: {
|
||||
id: sharedFileId,
|
||||
appid,
|
||||
sessionid: this.getSessionID()
|
||||
},
|
||||
source: 'steamcommunity'
|
||||
});
|
||||
|
||||
resolve();
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Unsubscribes from a sharedfile's comment section. Note: Checkbox on webpage does not update
|
||||
* @param {SteamID | String} userID - ID of the user associated to this sharedfile
|
||||
* @param {String} sharedFileId - ID of the sharedfile
|
||||
* @param {function} callback - Takes only an Error object/null as the first argument
|
||||
*/
|
||||
SteamCommunity.prototype.unsubscribeSharedFileComments = function(userID, sharedFileId, callback) {
|
||||
if (typeof userID === 'string') {
|
||||
userID = new SteamID(userID);
|
||||
}
|
||||
|
||||
return StdLib.Promises.callbackPromise(null, callback, true, async (resolve, reject) => {
|
||||
await this.httpRequest({
|
||||
method: 'POST',
|
||||
url: `https://steamcommunity.com/comment/PublishedFile_Public/unsubscribe/${userID.toString()}/${sharedFileId}/`,
|
||||
form: {
|
||||
count: 10,
|
||||
sessionid: this.getSessionID()
|
||||
}
|
||||
});
|
||||
|
||||
resolve();
|
||||
});
|
||||
};
|
||||
164
components/twofactor.js
Normal file
164
components/twofactor.js
Normal file
@@ -0,0 +1,164 @@
|
||||
const StdLib = require('@doctormckay/stdlib');
|
||||
const SteamTotp = require('steam-totp');
|
||||
|
||||
const SteamCommunity = require('../index.js');
|
||||
const Helpers = require('./helpers.js');
|
||||
|
||||
const ETwoFactorTokenType = {
|
||||
None: 0, // No token-based two-factor authentication
|
||||
ValveMobileApp: 1, // Tokens generated using Valve's special charset (5 digits, alphanumeric)
|
||||
ThirdParty: 2 // Tokens generated using literally everyone else's standard charset (6 digits, numeric). This is disabled on the backend.
|
||||
};
|
||||
|
||||
/**
|
||||
* @param {function} [callback]
|
||||
* @return {Promise<object>}
|
||||
*/
|
||||
SteamCommunity.prototype.enableTwoFactor = function(callback) {
|
||||
return StdLib.Promises.callbackPromise(null, callback, false, async (resolve, reject) => {
|
||||
this._verifyMobileAccessToken();
|
||||
|
||||
if (!this.mobileAccessToken) {
|
||||
return reject(new Error('No mobile access token available. Provide one by calling setMobileAppAccessToken()'));
|
||||
}
|
||||
|
||||
let {jsonBody} = await this.httpRequest({
|
||||
method: 'POST',
|
||||
url: `https://api.steampowered.com/ITwoFactorService/AddAuthenticator/v1/?access_token=${this.mobileAccessToken}`,
|
||||
// TODO: Send this as protobuf to more closely mimic official app behavior
|
||||
form: {
|
||||
steamid: this.steamID.getSteamID64(),
|
||||
authenticator_time: Math.floor(Date.now() / 1000),
|
||||
authenticator_type: ETwoFactorTokenType.ValveMobileApp,
|
||||
device_identifier: SteamTotp.getDeviceID(this.steamID),
|
||||
sms_phone_id: '1'
|
||||
},
|
||||
source: 'steamcommunity'
|
||||
});
|
||||
|
||||
|
||||
if (!jsonBody.response) {
|
||||
return reject(new Error('Malformed response'));
|
||||
}
|
||||
|
||||
if (jsonBody.response.status != 1) {
|
||||
let error = new Error(`Error ${jsonBody.response.status}`);
|
||||
error.eresult = jsonBody.response.status;
|
||||
return reject(error);
|
||||
}
|
||||
|
||||
resolve(jsonBody.response);
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* @param {string} secret
|
||||
* @param {string} activationCode
|
||||
* @param {function} [callback]
|
||||
* @return Promise<void>
|
||||
*/
|
||||
SteamCommunity.prototype.finalizeTwoFactor = function(secret, activationCode, callback) {
|
||||
return StdLib.Promises.callbackPromise(null, callback, false, async (resolve, reject) => {
|
||||
this._verifyMobileAccessToken();
|
||||
|
||||
if (!this.mobileAccessToken) {
|
||||
return reject(new Error('No mobile access token available. Provide one by calling setMobileAppAccessToken()'));
|
||||
}
|
||||
|
||||
let attemptsLeft = 30;
|
||||
let diff = 0;
|
||||
|
||||
await new Promise((resolve, reject) => {
|
||||
SteamTotp.getTimeOffset(function(err, offset, latency) {
|
||||
if (err) {
|
||||
return reject(err);
|
||||
}
|
||||
|
||||
diff = offset;
|
||||
resolve();
|
||||
});
|
||||
});
|
||||
|
||||
let finalize = async () => {
|
||||
let code = SteamTotp.generateAuthCode(secret, diff);
|
||||
|
||||
let {jsonBody} = this.httpRequest({
|
||||
method: 'POST',
|
||||
url: `https://api.steampowered.com/ITwoFactorService/FinalizeAddAuthenticator/v1/?access_token=${this.mobileAccessToken}`,
|
||||
form: {
|
||||
steamid: this.steamID.getSteamID64(),
|
||||
authenticator_code: code,
|
||||
authenticator_time: Math.floor(Date.now() / 1000),
|
||||
activation_code: activationCode
|
||||
},
|
||||
source: 'steamcommunity'
|
||||
});
|
||||
|
||||
if (!jsonBody.response) {
|
||||
return reject(new Error('Malformed response'));
|
||||
}
|
||||
|
||||
jsonBody = jsonBody.response;
|
||||
|
||||
if (jsonBody.server_time) {
|
||||
diff = jsonBody.server_time - Math.floor(Date.now() / 1000);
|
||||
}
|
||||
|
||||
if (jsonBody.status == SteamCommunity.EResult.TwoFactorActivationCodeMismatch) {
|
||||
return reject(new Error('Invalid activation code'));
|
||||
} else if (jsonBody.want_more) {
|
||||
if (--attemptsLeft <= 0) {
|
||||
// We made more than 30 attempts, something must be wrong
|
||||
return reject(Helpers.eresultError(SteamCommunity.EResult.Fail));
|
||||
}
|
||||
diff += 30;
|
||||
|
||||
finalize();
|
||||
} else if (!jsonBody.success) {
|
||||
return reject(new Error(`Error ${jsonBody.status}`));
|
||||
} else {
|
||||
resolve();
|
||||
}
|
||||
};
|
||||
|
||||
finalize();
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* @param {string} revocationCode
|
||||
* @param {function} [callback]
|
||||
* @return Promise<void>
|
||||
*/
|
||||
SteamCommunity.prototype.disableTwoFactor = function(revocationCode, callback) {
|
||||
return StdLib.Promises.callbackPromise(null, callback, false, async (resolve, reject) => {
|
||||
this._verifyMobileAccessToken();
|
||||
|
||||
if (!this.mobileAccessToken) {
|
||||
callback(new Error('No mobile access token available. Provide one by calling setMobileAppAccessToken()'));
|
||||
return;
|
||||
}
|
||||
|
||||
let {jsonBody} = await this.httpRequest({
|
||||
method: 'POST',
|
||||
url: `https://api.steampowered.com/ITwoFactorService/RemoveAuthenticator/v1/?access_token=${this.mobileAccessToken}`,
|
||||
form: {
|
||||
steamid: this.steamID.getSteamID64(),
|
||||
revocation_code: revocationCode,
|
||||
steamguard_scheme: 1
|
||||
},
|
||||
source: 'steamcommunity'
|
||||
});
|
||||
|
||||
if (!jsonBody.response) {
|
||||
return reject(new Error('Malformed response'));
|
||||
}
|
||||
|
||||
if (!jsonBody.response.success) {
|
||||
return reject(new Error('Request failed'));
|
||||
}
|
||||
|
||||
// success = true means it worked
|
||||
resolve();
|
||||
});
|
||||
};
|
||||
749
components/users.js
Normal file
749
components/users.js
Normal file
@@ -0,0 +1,749 @@
|
||||
const Cheerio = require('cheerio');
|
||||
const Crypto = require('crypto');
|
||||
const imageSize = require('image-size');
|
||||
const SteamID = require('steamid');
|
||||
|
||||
const SteamCommunity = require('../index.js');
|
||||
|
||||
const CEconItem = require('../classes/CEconItem.js');
|
||||
const Helpers = require('./helpers.js');
|
||||
|
||||
SteamCommunity.prototype.addFriend = function(userID, callback) {
|
||||
if (typeof userID === 'string') {
|
||||
userID = new SteamID(userID);
|
||||
}
|
||||
|
||||
this.httpRequestPost({
|
||||
url: 'https://steamcommunity.com/actions/AddFriendAjax',
|
||||
form: {
|
||||
accept_invite: 0,
|
||||
sessionID: this.getSessionID(),
|
||||
steamid: userID.toString()
|
||||
},
|
||||
json: true
|
||||
}, (err, response, body) => {
|
||||
if (!callback) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (err) {
|
||||
return callback(err);
|
||||
}
|
||||
|
||||
if (body.success) {
|
||||
callback(null);
|
||||
} else {
|
||||
callback(new Error('Unknown error'));
|
||||
}
|
||||
}, 'steamcommunity');
|
||||
};
|
||||
|
||||
SteamCommunity.prototype.acceptFriendRequest = function(userID, callback) {
|
||||
if (typeof userID === 'string') {
|
||||
userID = new SteamID(userID);
|
||||
}
|
||||
|
||||
this.httpRequestPost({
|
||||
url: 'https://steamcommunity.com/actions/AddFriendAjax',
|
||||
form: {
|
||||
accept_invite: 1,
|
||||
sessionID: this.getSessionID(),
|
||||
steamid: userID.toString()
|
||||
}
|
||||
}, (err, response, body) => {
|
||||
if (!callback) {
|
||||
return;
|
||||
}
|
||||
|
||||
callback(err || null);
|
||||
}, 'steamcommunity');
|
||||
};
|
||||
|
||||
SteamCommunity.prototype.removeFriend = function(userID, callback) {
|
||||
if (typeof userID === 'string') {
|
||||
userID = new SteamID(userID);
|
||||
}
|
||||
|
||||
this.httpRequestPost({
|
||||
url: 'https://steamcommunity.com/actions/RemoveFriendAjax',
|
||||
form: {
|
||||
sessionID: this.getSessionID(),
|
||||
steamid: userID.toString()
|
||||
}
|
||||
}, (err, response, body) => {
|
||||
if (!callback) {
|
||||
return;
|
||||
}
|
||||
|
||||
callback(err || null);
|
||||
}, 'steamcommunity');
|
||||
};
|
||||
|
||||
SteamCommunity.prototype.blockCommunication = function(userID, callback) {
|
||||
if (typeof userID === 'string') {
|
||||
userID = new SteamID(userID);
|
||||
}
|
||||
|
||||
this.httpRequestPost({
|
||||
url: 'https://steamcommunity.com/actions/BlockUserAjax',
|
||||
form: {
|
||||
sessionID: this.getSessionID(),
|
||||
steamid: userID.toString()
|
||||
}
|
||||
}, (err, response, body) => {
|
||||
if (!callback) {
|
||||
return;
|
||||
}
|
||||
|
||||
callback(err || null);
|
||||
}, 'steamcommunity');
|
||||
};
|
||||
|
||||
SteamCommunity.prototype.unblockCommunication = function(userID, callback) {
|
||||
if (typeof userID === 'string') {
|
||||
userID = new SteamID(userID);
|
||||
}
|
||||
|
||||
let form = {action: 'unignore'};
|
||||
form['friends[' + userID.toString() + ']'] = 1;
|
||||
|
||||
this._myProfile('friends/blocked/', form, (err, response, body) => {
|
||||
if (!callback) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (err || response.statusCode >= 400) {
|
||||
callback(err || new Error(`HTTP error ${response.statusCode}`));
|
||||
return;
|
||||
}
|
||||
|
||||
callback(null);
|
||||
});
|
||||
};
|
||||
|
||||
SteamCommunity.prototype.postUserComment = function(userID, message, callback) {
|
||||
if (typeof userID === 'string') {
|
||||
userID = new SteamID(userID);
|
||||
}
|
||||
|
||||
this.httpRequestPost({
|
||||
url: `https://steamcommunity.com/comment/Profile/post/${userID.toString()}/-1`,
|
||||
form: {
|
||||
comment: message,
|
||||
count: 1,
|
||||
sessionid: this.getSessionID()
|
||||
},
|
||||
json: true
|
||||
}, (err, response, body) => {
|
||||
if (!callback) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (err) {
|
||||
callback(err);
|
||||
return;
|
||||
}
|
||||
|
||||
if (body.success) {
|
||||
const $ = Cheerio.load(body.comments_html);
|
||||
const commentID = $('.commentthread_comment').attr('id').split('_')[1];
|
||||
|
||||
callback(null, commentID);
|
||||
} else if (body.error) {
|
||||
callback(new Error(body.error));
|
||||
} else {
|
||||
callback(new Error('Unknown error'));
|
||||
}
|
||||
}, 'steamcommunity');
|
||||
};
|
||||
|
||||
SteamCommunity.prototype.deleteUserComment = function(userID, commentID, callback) {
|
||||
if (typeof userID === 'string') {
|
||||
userID = new SteamID(userID);
|
||||
}
|
||||
|
||||
this.httpRequestPost({
|
||||
url: `https://steamcommunity.com/comment/Profile/delete/${userID.toString()}/-1`,
|
||||
form: {
|
||||
gidcomment: commentID,
|
||||
start: 0,
|
||||
count: 1,
|
||||
sessionid: this.getSessionID(),
|
||||
feature2: -1
|
||||
},
|
||||
json: true
|
||||
}, (err, response, body) => {
|
||||
if (!callback) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (err) {
|
||||
callback(err);
|
||||
return;
|
||||
}
|
||||
|
||||
if (body.success && !body.comments_html.includes(commentID)) {
|
||||
callback(null);
|
||||
} else if (body.error) {
|
||||
callback(new Error(body.error));
|
||||
} else if (body.comments_html.includes(commentID)) {
|
||||
callback(new Error('Failed to delete comment'));
|
||||
} else {
|
||||
callback(new Error('Unknown error'));
|
||||
}
|
||||
}, 'steamcommunity');
|
||||
};
|
||||
|
||||
SteamCommunity.prototype.getUserComments = function(userID, options, callback) {
|
||||
if (typeof userID === 'string') {
|
||||
userID = new SteamID(userID);
|
||||
}
|
||||
|
||||
if (typeof options === 'function') {
|
||||
callback = options;
|
||||
options = {};
|
||||
}
|
||||
|
||||
let form = Object.assign({
|
||||
start: 0,
|
||||
count: 0,
|
||||
feature2: -1,
|
||||
sessionid: this.getSessionID()
|
||||
}, options);
|
||||
|
||||
this.httpRequestPost({
|
||||
url: `https://steamcommunity.com/comment/Profile/render/${userID.toString()}/-1`,
|
||||
form,
|
||||
json: true
|
||||
}, (err, response, body) => {
|
||||
if (!callback) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (err) {
|
||||
callback(err);
|
||||
return;
|
||||
}
|
||||
|
||||
if (body.success) {
|
||||
const $ = Cheerio.load(body.comments_html);
|
||||
const comments = $('.commentthread_comment.responsive_body_text[id]').map((i, elem) => {
|
||||
let $elem = $(elem),
|
||||
$commentContent = $elem.find('.commentthread_comment_text');
|
||||
return {
|
||||
id: $elem.attr('id').split('_')[1],
|
||||
author: {
|
||||
steamID: new SteamID('[U:1:' + $elem.find('[data-miniprofile]').data('miniprofile') + ']'),
|
||||
name: $elem.find('bdi').text(),
|
||||
avatar: $elem.find('.playerAvatar img[src]').attr('src'),
|
||||
state: $elem.find('.playerAvatar').attr('class').split(' ').pop()
|
||||
},
|
||||
date: new Date($elem.find('.commentthread_comment_timestamp').data('timestamp') * 1000),
|
||||
text: $commentContent.text().trim(),
|
||||
html: $commentContent.html().trim()
|
||||
};
|
||||
}).get();
|
||||
|
||||
callback(null, comments, body.total_count);
|
||||
} else if (body.error) {
|
||||
callback(new Error(body.error));
|
||||
} else {
|
||||
callback(new Error('Unknown error'));
|
||||
}
|
||||
}, 'steamcommunity');
|
||||
};
|
||||
|
||||
SteamCommunity.prototype.inviteUserToGroup = function(userID, groupID, callback) {
|
||||
if (typeof userID === 'string') {
|
||||
userID = new SteamID(userID);
|
||||
}
|
||||
|
||||
this.httpRequestPost({
|
||||
url: 'https://steamcommunity.com/actions/GroupInvite',
|
||||
form: {
|
||||
group: groupID.toString(),
|
||||
invitee: userID.toString(),
|
||||
json: 1,
|
||||
sessionID: this.getSessionID(),
|
||||
type: 'groupInvite'
|
||||
},
|
||||
json: true
|
||||
}, (err, response, body) => {
|
||||
if (!callback) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (err) {
|
||||
callback(err);
|
||||
return;
|
||||
}
|
||||
|
||||
if (body.results == 'OK') {
|
||||
callback(null);
|
||||
} else if (body.results) {
|
||||
callback(new Error(body.results));
|
||||
} else {
|
||||
callback(new Error('Unknown error'));
|
||||
}
|
||||
}, 'steamcommunity');
|
||||
};
|
||||
|
||||
SteamCommunity.prototype.getUserAliases = function(userID, callback) {
|
||||
if (typeof userID === 'string') {
|
||||
userID = new SteamID(userID);
|
||||
}
|
||||
|
||||
this.httpRequestGet({
|
||||
url: `https://steamcommunity.com/profiles/${userID.getSteamID64()}/ajaxaliases`,
|
||||
json: true
|
||||
}, (err, response, body) => {
|
||||
if (err) {
|
||||
callback(err);
|
||||
return;
|
||||
}
|
||||
|
||||
if (typeof body !== 'object') {
|
||||
callback(new Error('Malformed response'));
|
||||
return;
|
||||
}
|
||||
|
||||
callback(null, body.map((entry) => {
|
||||
entry.timechanged = Helpers.decodeSteamTime(entry.timechanged);
|
||||
return entry;
|
||||
}));
|
||||
}, 'steamcommunity');
|
||||
};
|
||||
|
||||
/**
|
||||
* Get the background URL of user's profile.
|
||||
* @param {SteamID|string} userID - The user's SteamID as a SteamID object or a string which can parse into one
|
||||
* @param {function} callback
|
||||
*/
|
||||
SteamCommunity.prototype.getUserProfileBackground = function(userID, callback) {
|
||||
if (typeof userID === 'string') {
|
||||
userID = new SteamID(userID);
|
||||
}
|
||||
|
||||
this.httpRequest(`https://steamcommunity.com/profiles/${userID.getSteamID64()}`, (err, response, body) => {
|
||||
if (err) {
|
||||
callback(err);
|
||||
return;
|
||||
}
|
||||
|
||||
let $ = Cheerio.load(body);
|
||||
|
||||
let $privateProfileInfo = $('.profile_private_info');
|
||||
if ($privateProfileInfo.length > 0) {
|
||||
callback(new Error($privateProfileInfo.text().trim()));
|
||||
return;
|
||||
}
|
||||
|
||||
if ($('body').hasClass('has_profile_background')) {
|
||||
let backgroundUrl = $('div.profile_background_image_content').css('background-image');
|
||||
let matcher = backgroundUrl.match(/\(([^)]+)\)/);
|
||||
|
||||
if (matcher.length != 2 || !matcher[1].length) {
|
||||
callback(new Error('Malformed response'));
|
||||
} else {
|
||||
callback(null, matcher[1]);
|
||||
}
|
||||
} else {
|
||||
callback(null, null);
|
||||
}
|
||||
}, 'steamcommunity');
|
||||
};
|
||||
|
||||
SteamCommunity.prototype.getUserInventoryContexts = function(userID, callback) {
|
||||
if (typeof userID === 'string') {
|
||||
userID = new SteamID(userID);
|
||||
}
|
||||
|
||||
if (typeof userID === 'function') {
|
||||
callback = userID;
|
||||
userID = this.steamID;
|
||||
}
|
||||
|
||||
if (!userID) {
|
||||
callback(new Error('No SteamID specified and not logged in'));
|
||||
return;
|
||||
}
|
||||
|
||||
this.httpRequest(`https://steamcommunity.com/profiles/${userID.getSteamID64()}/inventory/`, (err, response, body) => {
|
||||
if (err) {
|
||||
callback(err);
|
||||
return;
|
||||
}
|
||||
|
||||
let match = body.match(/var g_rgAppContextData = ([^\n]+);\r?\n/);
|
||||
if (!match) {
|
||||
callback(new Error('Malformed response'));
|
||||
return;
|
||||
}
|
||||
|
||||
let data;
|
||||
try {
|
||||
data = JSON.parse(match[1]);
|
||||
} catch (e) {
|
||||
callback(new Error('Malformed response'));
|
||||
return;
|
||||
}
|
||||
|
||||
if (Object.keys(data).length == 0) {
|
||||
if (body.match(/inventory is currently private\./)) {
|
||||
callback(new Error('Private inventory'));
|
||||
return;
|
||||
}
|
||||
|
||||
if (body.match(/profile_private_info/)) {
|
||||
callback(new Error('Private profile'));
|
||||
return;
|
||||
}
|
||||
|
||||
// If they truly have no items in their inventory, Steam will send g_rgAppContextData as [] instead of {}.
|
||||
data = {};
|
||||
}
|
||||
|
||||
callback(null, data);
|
||||
}, 'steamcommunity');
|
||||
};
|
||||
|
||||
/**
|
||||
* Get the contents of a user's inventory context.
|
||||
* @deprecated Use getUserInventoryContents instead
|
||||
* @param {SteamID|string} userID - The user's SteamID as a SteamID object or a string which can parse into one
|
||||
* @param {int} appID - The Steam application ID of the game for which you want an inventory
|
||||
* @param {int} contextID - The ID of the "context" within the game you want to retrieve
|
||||
* @param {boolean} tradableOnly - true to get only tradable items and currencies
|
||||
* @param {function} callback
|
||||
*/
|
||||
SteamCommunity.prototype.getUserInventory = function(userID, appID, contextID, tradableOnly, callback) {
|
||||
if (typeof userID === 'string') {
|
||||
userID = new SteamID(userID);
|
||||
}
|
||||
|
||||
const get = (inventory, currency, start) => {
|
||||
this.httpRequest({
|
||||
url: `https://steamcommunity.com${endpoint}/inventory/json/${appID}/${contextID}`,
|
||||
headers: {
|
||||
Referer: `https://steamcommunity.com${endpoint}/inventory`
|
||||
},
|
||||
qs: {
|
||||
start: start,
|
||||
trading: tradableOnly ? 1 : undefined
|
||||
},
|
||||
json: true
|
||||
}, (err, response, body) => {
|
||||
if (err) {
|
||||
callback(err);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!body || !body.success || !body.rgInventory || !body.rgDescriptions || !body.rgCurrency) {
|
||||
if (body) {
|
||||
callback(new Error(body.Error || 'Malformed response'));
|
||||
} else {
|
||||
callback(new Error('Malformed response'));
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
for (let i in body.rgInventory) {
|
||||
inventory.push(new CEconItem(body.rgInventory[i], body.rgDescriptions, contextID));
|
||||
}
|
||||
|
||||
for (let i in body.rgCurrency) {
|
||||
currency.push(new CEconItem(body.rgCurrency[i], body.rgDescriptions, contextID));
|
||||
}
|
||||
|
||||
if (body.more) {
|
||||
let match = response.request.uri.href.match(/\/(profiles|id)\/([^/]+)\//);
|
||||
if (match) {
|
||||
endpoint = `/${match[1]}/${match[2]}`;
|
||||
}
|
||||
|
||||
get(inventory, currency, body.more_start);
|
||||
} else {
|
||||
callback(null, inventory, currency);
|
||||
}
|
||||
}, 'steamcommunity');
|
||||
};
|
||||
|
||||
let endpoint = `/profiles/${userID.getSteamID64()}`;
|
||||
get([], []);
|
||||
};
|
||||
|
||||
/**
|
||||
* Get the contents of a user's inventory context.
|
||||
* @param {SteamID|string} userID - The user's SteamID as a SteamID object or a string which can parse into one
|
||||
* @param {int} appID - The Steam application ID of the game for which you want an inventory
|
||||
* @param {int} contextID - The ID of the "context" within the game you want to retrieve
|
||||
* @param {boolean} tradableOnly - true to get only tradable items and currencies
|
||||
* @param {string} [language] - The language of item descriptions to return. Omit for default (which may either be English or your account's chosen language)
|
||||
* @param {function} callback
|
||||
*/
|
||||
SteamCommunity.prototype.getUserInventoryContents = function(userID, appID, contextID, tradableOnly, language, callback) {
|
||||
if (typeof language === 'function') {
|
||||
callback = language;
|
||||
language = 'english';
|
||||
}
|
||||
|
||||
if (!userID) {
|
||||
callback(new Error('The user\'s SteamID is invalid or missing.'));
|
||||
return;
|
||||
}
|
||||
|
||||
if (typeof userID === 'string') {
|
||||
userID = new SteamID(userID);
|
||||
}
|
||||
|
||||
// A bit of optimization; objects are hash tables so it's more efficient to look up by key than to iterate an array
|
||||
let quickDescriptionLookup = {};
|
||||
|
||||
const getDescription = (descriptions, classID, instanceID) => {
|
||||
let key = classID + '_' + (instanceID || '0'); // instanceID can be undefined, in which case it's 0.
|
||||
|
||||
if (quickDescriptionLookup[key]) {
|
||||
return quickDescriptionLookup[key];
|
||||
}
|
||||
|
||||
for (let i = 0; i < descriptions.length; i++) {
|
||||
quickDescriptionLookup[descriptions[i].classid + '_' + (descriptions[i].instanceid || '0')] = descriptions[i];
|
||||
}
|
||||
|
||||
return quickDescriptionLookup[key];
|
||||
};
|
||||
|
||||
const get = (inventory, currency, start) => {
|
||||
this.httpRequest({
|
||||
url: `https://steamcommunity.com/inventory/${userID.getSteamID64()}/${appID}/${contextID}`,
|
||||
headers: {
|
||||
Referer: `https://steamcommunity.com/profiles/${userID.getSteamID64()}/inventory`
|
||||
},
|
||||
qs: {
|
||||
l: language, // Default language
|
||||
count: 2000, // Max items per 'page'
|
||||
start_assetid: start
|
||||
},
|
||||
json: true
|
||||
}, (err, response, body) => {
|
||||
if (err) {
|
||||
if (err.message == 'HTTP error 403' && body === null) {
|
||||
// 403 with a body of "null" means the inventory/profile is private.
|
||||
if (this.steamID && userID.getSteamID64() == this.steamID.getSteamID64()) {
|
||||
// We can never get private profile error for our own inventory!
|
||||
this._notifySessionExpired(err);
|
||||
}
|
||||
|
||||
callback(new Error('This profile is private.'));
|
||||
return;
|
||||
}
|
||||
|
||||
if (err.message == 'HTTP error 500' && body && body.error) {
|
||||
err = new Error(body.error);
|
||||
|
||||
let match = body.error.match(/^(.+) \((\d+)\)$/);
|
||||
if (match) {
|
||||
err.message = match[1];
|
||||
err.eresult = match[2];
|
||||
callback(err);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
callback(err);
|
||||
return;
|
||||
}
|
||||
|
||||
if (body && body.success && body.total_inventory_count === 0) {
|
||||
// Empty inventory
|
||||
callback(null, [], [], 0);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!body || !body.success || !body.assets || !body.descriptions) {
|
||||
if (body) {
|
||||
// Dunno if the error/Error property even exists on this new endpoint
|
||||
callback(new Error(body.error || body.Error || 'Malformed response'));
|
||||
} else {
|
||||
callback(new Error('Malformed response'));
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
for (let i = 0; i < body.assets.length; i++) {
|
||||
let description = getDescription(body.descriptions, body.assets[i].classid, body.assets[i].instanceid);
|
||||
|
||||
if (!tradableOnly || (description && description.tradable)) {
|
||||
body.assets[i].pos = pos++;
|
||||
(body.assets[i].currencyid ? currency : inventory).push(new CEconItem(body.assets[i], description, contextID));
|
||||
}
|
||||
}
|
||||
|
||||
if (body.more_items) {
|
||||
get(inventory, currency, body.last_assetid);
|
||||
} else {
|
||||
callback(null, inventory, currency, body.total_inventory_count);
|
||||
}
|
||||
}, 'steamcommunity');
|
||||
};
|
||||
|
||||
let pos = 1;
|
||||
get([], []);
|
||||
};
|
||||
|
||||
/**
|
||||
* Upload an image to Steam and send it to another user over Steam chat.
|
||||
* @param {SteamID|string} userID - Either a SteamID object or a string that can parse into one
|
||||
* @param {Buffer} imageContentsBuffer - The image contents, as a Buffer
|
||||
* @param {{spoiler?: boolean}} [options]
|
||||
* @param {function} callback
|
||||
*/
|
||||
SteamCommunity.prototype.sendImageToUser = function(userID, imageContentsBuffer, options, callback) {
|
||||
if (typeof options == 'function') {
|
||||
callback = options;
|
||||
options = {};
|
||||
}
|
||||
|
||||
options = options || {};
|
||||
|
||||
if (!userID) {
|
||||
callback(new Error('The user\'s SteamID is invalid or missing'));
|
||||
return;
|
||||
}
|
||||
|
||||
if (typeof userID == 'string') {
|
||||
userID = new SteamID(userID);
|
||||
}
|
||||
|
||||
if (!Buffer.isBuffer(imageContentsBuffer)) {
|
||||
callback(new Error('The image contents must be a Buffer containing an image'));
|
||||
return;
|
||||
}
|
||||
|
||||
let imageDetails = null;
|
||||
try {
|
||||
imageDetails = imageSize(imageContentsBuffer);
|
||||
} catch (ex) {
|
||||
callback(ex);
|
||||
return;
|
||||
}
|
||||
|
||||
let imageHash = Crypto.createHash('sha1');
|
||||
imageHash.update(imageContentsBuffer);
|
||||
imageHash = imageHash.digest('hex');
|
||||
|
||||
let filename = Date.now() + '_image.' + imageDetails.type;
|
||||
|
||||
this.httpRequestPost({
|
||||
url: 'https://steamcommunity.com/chat/beginfileupload/?l=english',
|
||||
headers: {
|
||||
referer: 'https://steamcommunity.com/chat/'
|
||||
},
|
||||
formData: { // it's multipart
|
||||
sessionid: this.getSessionID(),
|
||||
l: 'english',
|
||||
file_size: imageContentsBuffer.length,
|
||||
file_name: filename,
|
||||
file_sha: imageHash,
|
||||
file_image_width: imageDetails.width,
|
||||
file_image_height: imageDetails.height,
|
||||
file_type: 'image/' + (imageDetails.type == 'jpg' ? 'jpeg' : imageDetails.type)
|
||||
},
|
||||
json: true
|
||||
}, (err, res, body) => {
|
||||
if (err) {
|
||||
if (body && body.success) {
|
||||
let err2 = Helpers.eresultError(body.success);
|
||||
if (body.message) {
|
||||
err2.message = body.message;
|
||||
}
|
||||
callback(err2);
|
||||
} else {
|
||||
callback(err);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (body.success != 1) {
|
||||
callback(Helpers.eresultError(body.success));
|
||||
return;
|
||||
}
|
||||
|
||||
let hmac = body.hmac;
|
||||
let timestamp = body.timestamp;
|
||||
let startResult = body.result;
|
||||
|
||||
if (!startResult || !startResult.ugcid || !startResult.url_host || !startResult.request_headers) {
|
||||
callback(new Error('Malformed response'));
|
||||
return;
|
||||
}
|
||||
|
||||
// Okay, now we need to PUT the file to the provided URL
|
||||
let uploadUrl = (startResult.use_https ? 'https' : 'http') + '://' + startResult.url_host + startResult.url_path;
|
||||
let headers = {};
|
||||
startResult.request_headers.forEach((header) => {
|
||||
headers[header.name.toLowerCase()] = header.value;
|
||||
});
|
||||
|
||||
this.httpRequest({
|
||||
url: uploadUrl,
|
||||
method: 'PUT',
|
||||
headers,
|
||||
body: imageContentsBuffer
|
||||
}, (err, res, body) => {
|
||||
if (err) {
|
||||
callback(err);
|
||||
return;
|
||||
}
|
||||
|
||||
// Now we need to commit the upload
|
||||
this.httpRequestPost({
|
||||
url: 'https://steamcommunity.com/chat/commitfileupload/',
|
||||
headers: {
|
||||
referer: 'https://steamcommunity.com/chat/'
|
||||
},
|
||||
formData: { // it's multipart again
|
||||
sessionid: this.getSessionID(),
|
||||
l: 'english',
|
||||
file_name: filename,
|
||||
file_sha: imageHash,
|
||||
success: '1',
|
||||
ugcid: startResult.ugcid,
|
||||
file_type: 'image/' + (imageDetails.type == 'jpg' ? 'jpeg' : imageDetails.type),
|
||||
file_image_width: imageDetails.width,
|
||||
file_image_height: imageDetails.height,
|
||||
timestamp,
|
||||
hmac,
|
||||
friend_steamid: userID.getSteamID64(),
|
||||
spoiler: options.spoiler ? '1' : '0'
|
||||
},
|
||||
json: true
|
||||
}, (err, res, body) => {
|
||||
if (err) {
|
||||
callback(err);
|
||||
return;
|
||||
}
|
||||
|
||||
if (body.success != 1) {
|
||||
callback(Helpers.eresultError(body.success));
|
||||
return;
|
||||
}
|
||||
|
||||
if (body.result.success != 1) {
|
||||
// lol valve
|
||||
callback(Helpers.eresultError(body.result.success));
|
||||
return;
|
||||
}
|
||||
|
||||
if (!body.result.details || !body.result.details.url) {
|
||||
callback(new Error('Malformed response'));
|
||||
return;
|
||||
}
|
||||
|
||||
callback(null, body.result.details.url);
|
||||
}, 'steamcommunity');
|
||||
}, 'steamcommunity');
|
||||
}, 'steamcommunity');
|
||||
};
|
||||
109
components/webapi.js
Normal file
109
components/webapi.js
Normal file
@@ -0,0 +1,109 @@
|
||||
const StdLib = require('@doctormckay/stdlib');
|
||||
|
||||
const SteamCommunity = require('../index.js');
|
||||
|
||||
const Helpers = require('./helpers.js');
|
||||
|
||||
/**
|
||||
* @param {string} domain
|
||||
* @param {function} [callback]
|
||||
* @return Promise<{key: string}>
|
||||
*/
|
||||
SteamCommunity.prototype.getWebApiKey = function(domain, callback) {
|
||||
return StdLib.Promises.callbackPromise(['key'], callback, false, async (resolve, reject) => {
|
||||
let {textBody} = await this.httpRequest({
|
||||
method: 'GET',
|
||||
url: 'https://steamcommunity.com/dev/apikey?l=english',
|
||||
followRedirect: false,
|
||||
source: 'steamcommunity'
|
||||
});
|
||||
|
||||
if (textBody.includes('<h2>Access Denied</h2>')) {
|
||||
return reject(new Error('Access Denied'));
|
||||
}
|
||||
|
||||
if (textBody.includes('You must have a validated email address to create a Steam Web API key.')) {
|
||||
return reject(new Error('You must have a validated email address to create a Steam Web API key.'));
|
||||
}
|
||||
|
||||
let match = textBody.match(/<p>Key: ([0-9A-F]+)<\/p>/);
|
||||
if (match) {
|
||||
// We already have an API key registered
|
||||
return resolve({key: match[1]});
|
||||
}
|
||||
|
||||
// We need to register a new API key
|
||||
await this.httpRequest({
|
||||
method: 'POST',
|
||||
url: 'https://steamcommunity.com/dev/registerkey?l=english',
|
||||
form: {
|
||||
domain,
|
||||
agreeToTerms: 'agreed',
|
||||
sessionid: this.getSessionID(),
|
||||
Submit: 'Register'
|
||||
},
|
||||
source: 'steamcommunity'
|
||||
});
|
||||
|
||||
resolve({key: await this.getWebApiKey(domain)});
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Sets an access_token generated by steam-session using EAuthTokenPlatformType.MobileApp.
|
||||
* Required for some operations such as 2FA enabling and disabling.
|
||||
* This will throw an Error if the provided token is not valid, was not generated for the MobileApp platform, is expired,
|
||||
* or does not belong to the logged-in user account.
|
||||
*
|
||||
* @param {string} token
|
||||
*/
|
||||
SteamCommunity.prototype.setMobileAppAccessToken = function(token) {
|
||||
if (!this.steamID) {
|
||||
throw new Error('Log on to steamcommunity before setting a mobile app access token');
|
||||
}
|
||||
|
||||
let decodedToken = Helpers.decodeJwt(token);
|
||||
|
||||
if (!decodedToken.iss || !decodedToken.sub || !decodedToken.aud || !decodedToken.exp) {
|
||||
throw new Error('Provided value is not a valid Steam access token');
|
||||
}
|
||||
|
||||
if (decodedToken.iss == 'steam') {
|
||||
throw new Error('Provided token is a refresh token, not an access token');
|
||||
}
|
||||
|
||||
if (decodedToken.sub != this.steamID.getSteamID64()) {
|
||||
throw new Error(`Provided token belongs to account ${decodedToken.sub}, but we are logged into ${this.steamID.getSteamID64()}`);
|
||||
}
|
||||
|
||||
if (decodedToken.exp < Math.floor(Date.now() / 1000)) {
|
||||
throw new Error('Provided token is expired');
|
||||
}
|
||||
|
||||
if ((decodedToken.aud || []).indexOf('mobile') == -1) {
|
||||
throw new Error('Provided token is not valid for MobileApp platform type');
|
||||
}
|
||||
|
||||
this.mobileAccessToken = token;
|
||||
};
|
||||
|
||||
/**
|
||||
* Verifies that the mobile access token we already have set is still valid for current login.
|
||||
*
|
||||
* @private
|
||||
*/
|
||||
SteamCommunity.prototype._verifyMobileAccessToken = function() {
|
||||
if (!this.mobileAccessToken) {
|
||||
// No access token, so nothing to do here.
|
||||
return;
|
||||
}
|
||||
|
||||
let decodedToken = Helpers.decodeJwt(this.mobileAccessToken);
|
||||
|
||||
let isTokenInvalid = decodedToken.sub != this.steamID.getSteamID64() // SteamID doesn't match
|
||||
|| decodedToken.exp < Math.floor(Date.now() / 1000); // Token is expired
|
||||
|
||||
if (isTokenInvalid) {
|
||||
delete this.mobileAccessToken;
|
||||
}
|
||||
};
|
||||
35
examples/README.md
Normal file
35
examples/README.md
Normal file
@@ -0,0 +1,35 @@
|
||||
# node-steamcommunity examples
|
||||
|
||||
The files in this directory are example scripts that you can use as a getting-started point for using node-steamcommunity.
|
||||
|
||||
## Enable or Disable Two-Factor Authentication
|
||||
|
||||
If you need to enable or disable 2FA on your bot account, you can use enable_twofactor.js and disable_twofactor.js to do so.
|
||||
The way that you're intended to use these scripts is by cloning the repository locally, and then running them directly
|
||||
from this examples directory.
|
||||
|
||||
For example:
|
||||
|
||||
```shell
|
||||
git clone https://github.com/DoctorMcKay/node-steamcommunity node-steamcommunity
|
||||
cd node-steamcommunity
|
||||
npm install
|
||||
cd examples
|
||||
node enable_twofactor.js
|
||||
```
|
||||
|
||||
## Accept All Confirmations
|
||||
|
||||
If you need to accept trade or market confirmations on your bot account for which you have your identity secret, you can
|
||||
use accept_all_confirmations.js to do so. The way that you're intended to use this script is by cloning the repository
|
||||
locally, and then running it directly from this examples directory.
|
||||
|
||||
For example:
|
||||
|
||||
```shell
|
||||
git clone https://github.com/DoctorMcKay/node-steamcommunity node-steamcommunity
|
||||
cd node-steamcommunity
|
||||
npm install
|
||||
cd examples
|
||||
node accept_all_confirmations.js
|
||||
```
|
||||
173
examples/accept_all_confirmations.js
Normal file
173
examples/accept_all_confirmations.js
Normal file
@@ -0,0 +1,173 @@
|
||||
// If you aren't running this script inside of the repository, replace the following line with:
|
||||
// const SteamCommunity = require('steamcommunity');
|
||||
const SteamCommunity = require('../index.js');
|
||||
const SteamSession = require('steam-session');
|
||||
const SteamTotp = require('steam-totp');
|
||||
const ReadLine = require('readline');
|
||||
|
||||
const EConfirmationType = SteamCommunity.ConfirmationType;
|
||||
|
||||
let g_AbortPromptFunc = null;
|
||||
|
||||
let community = new SteamCommunity();
|
||||
|
||||
main();
|
||||
async function main() {
|
||||
let accountName = await promptAsync('Username: ');
|
||||
let password = await promptAsync('Password (hidden): ', true);
|
||||
|
||||
// Create a LoginSession for us to use to attempt to log into steam
|
||||
let session = new SteamSession.LoginSession(SteamSession.EAuthTokenPlatformType.MobileApp);
|
||||
|
||||
// Go ahead and attach our event handlers before we do anything else.
|
||||
session.on('authenticated', async () => {
|
||||
abortPrompt();
|
||||
|
||||
let cookies = await session.getWebCookies();
|
||||
community.setCookies(cookies);
|
||||
|
||||
doConfirmations();
|
||||
});
|
||||
|
||||
session.on('timeout', () => {
|
||||
abortPrompt();
|
||||
console.log('This login attempt has timed out.');
|
||||
});
|
||||
|
||||
session.on('error', (err) => {
|
||||
abortPrompt();
|
||||
|
||||
// This should ordinarily not happen. This only happens in case there's some kind of unexpected error while
|
||||
// polling, e.g. the network connection goes down or Steam chokes on something.
|
||||
|
||||
console.log(`ERROR: This login attempt has failed! ${err.message}`);
|
||||
});
|
||||
|
||||
// Start our login attempt
|
||||
let startResult = await session.startWithCredentials({accountName, password});
|
||||
if (startResult.actionRequired) {
|
||||
// Some Steam Guard action is required. We only care about email and device codes; in theory an
|
||||
// EmailConfirmation and/or DeviceConfirmation action could be possible, but we're just going to ignore those.
|
||||
// If the user does receive a confirmation and accepts it, LoginSession will detect and handle that automatically.
|
||||
// The only consequence of ignoring it here is that we don't print a message to the user indicating that they
|
||||
// could accept an email or device confirmation.
|
||||
|
||||
let codeActionTypes = [SteamSession.EAuthSessionGuardType.EmailCode, SteamSession.EAuthSessionGuardType.DeviceCode];
|
||||
let codeAction = startResult.validActions.find(action => codeActionTypes.includes(action.type));
|
||||
if (codeAction) {
|
||||
if (codeAction.type == SteamSession.EAuthSessionGuardType.EmailCode) {
|
||||
// We wouldn't expect this to happen since mobile confirmations are only possible with 2FA enabled, but just in case...
|
||||
console.log(`A code has been sent to your email address at ${codeAction.detail}.`);
|
||||
} else {
|
||||
console.log('You need to provide a Steam Guard Mobile Authenticator code.');
|
||||
}
|
||||
|
||||
let code = await promptAsync('Code or Shared Secret: ');
|
||||
if (code) {
|
||||
// The code might've been a shared secret
|
||||
if (code.length > 10) {
|
||||
code = SteamTotp.getAuthCode(code);
|
||||
}
|
||||
await session.submitSteamGuardCode(code);
|
||||
}
|
||||
|
||||
// If we fall through here without submitting a Steam Guard code, that means one of two things:
|
||||
// 1. The user pressed enter without providing a code, in which case the script will simply exit
|
||||
// 2. The user approved a device/email confirmation, in which case 'authenticated' was emitted and the prompt was canceled
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function doConfirmations() {
|
||||
let identitySecret = await promptAsync('Identity Secret: ');
|
||||
|
||||
let confs = await new Promise((resolve, reject) => {
|
||||
let time = SteamTotp.time();
|
||||
let key = SteamTotp.getConfirmationKey(identitySecret, time, 'conf');
|
||||
community.getConfirmations(time, key, (err, confs) => {
|
||||
if (err) {
|
||||
return reject(err);
|
||||
}
|
||||
|
||||
resolve(confs);
|
||||
});
|
||||
});
|
||||
|
||||
console.log(`Found ${confs.length} outstanding confirmations.`);
|
||||
|
||||
// We need to track the previous timestamp we used, as we cannot reuse timestamps.
|
||||
let previousTime = 0;
|
||||
|
||||
for (let i = 0; i < confs.length; i++) {
|
||||
let conf = confs[i];
|
||||
|
||||
process.stdout.write(`Accepting confirmation for ${EConfirmationType[conf.type]} - ${conf.title}... `);
|
||||
|
||||
try {
|
||||
await new Promise((resolve, reject) => {
|
||||
let time = SteamTotp.time();
|
||||
if (time == previousTime) {
|
||||
time++;
|
||||
}
|
||||
|
||||
previousTime = time;
|
||||
let key = SteamTotp.getConfirmationKey(identitySecret, time, 'allow');
|
||||
conf.respond(time, key, true, (err) => {
|
||||
err ? reject(err) : resolve();
|
||||
});
|
||||
});
|
||||
|
||||
console.log('success');
|
||||
} catch (ex) {
|
||||
console.log(`error: ${ex.message}`);
|
||||
}
|
||||
|
||||
// sleep 500ms so we don't run too far away from the current timestamp
|
||||
await new Promise(resolve => setTimeout(resolve, 500));
|
||||
}
|
||||
|
||||
console.log('Finished processing confirmations');
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
// Nothing interesting below here, just code for prompting for input from the console.
|
||||
|
||||
function promptAsync(question, sensitiveInput = false) {
|
||||
return new Promise((resolve) => {
|
||||
let rl = ReadLine.createInterface({
|
||||
input: process.stdin,
|
||||
output: sensitiveInput ? null : process.stdout,
|
||||
terminal: true
|
||||
});
|
||||
|
||||
g_AbortPromptFunc = () => {
|
||||
rl.close();
|
||||
resolve('');
|
||||
};
|
||||
|
||||
if (sensitiveInput) {
|
||||
// We have to write the question manually if we didn't give readline an output stream
|
||||
process.stdout.write(question);
|
||||
}
|
||||
|
||||
rl.question(question, (result) => {
|
||||
if (sensitiveInput) {
|
||||
// We have to manually print a newline
|
||||
process.stdout.write('\n');
|
||||
}
|
||||
|
||||
g_AbortPromptFunc = null;
|
||||
rl.close();
|
||||
resolve(result);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function abortPrompt() {
|
||||
if (!g_AbortPromptFunc) {
|
||||
return;
|
||||
}
|
||||
|
||||
g_AbortPromptFunc();
|
||||
process.stdout.write('\n');
|
||||
}
|
||||
135
examples/disable_twofactor.js
Normal file
135
examples/disable_twofactor.js
Normal file
@@ -0,0 +1,135 @@
|
||||
// If you aren't running this script inside of the repository, replace the following line with:
|
||||
// const SteamCommunity = require('steamcommunity');
|
||||
const SteamCommunity = require('../index.js');
|
||||
const SteamSession = require('steam-session');
|
||||
const ReadLine = require('readline');
|
||||
|
||||
let g_AbortPromptFunc = null;
|
||||
|
||||
let community = new SteamCommunity();
|
||||
|
||||
main();
|
||||
async function main() {
|
||||
let accountName = await promptAsync('Username: ');
|
||||
let password = await promptAsync('Password (hidden): ', true);
|
||||
|
||||
// Create a LoginSession for us to use to attempt to log into steam
|
||||
let session = new SteamSession.LoginSession(SteamSession.EAuthTokenPlatformType.MobileApp);
|
||||
|
||||
// Go ahead and attach our event handlers before we do anything else.
|
||||
session.on('authenticated', async () => {
|
||||
abortPrompt();
|
||||
|
||||
let accessToken = session.accessToken;
|
||||
let cookies = await session.getWebCookies();
|
||||
|
||||
community.setCookies(cookies);
|
||||
community.setMobileAppAccessToken(accessToken);
|
||||
|
||||
// Enabling or disabling 2FA is presently the only action in node-steamcommunity which requires an access token.
|
||||
// In all other cases, using `community.setCookies(cookies)` is all you need to do in order to be logged in,
|
||||
// although there's never any harm in setting a mobile app access token.
|
||||
|
||||
doRevoke();
|
||||
});
|
||||
|
||||
session.on('timeout', () => {
|
||||
abortPrompt();
|
||||
console.log('This login attempt has timed out.');
|
||||
});
|
||||
|
||||
session.on('error', (err) => {
|
||||
abortPrompt();
|
||||
|
||||
// This should ordinarily not happen. This only happens in case there's some kind of unexpected error while
|
||||
// polling, e.g. the network connection goes down or Steam chokes on something.
|
||||
|
||||
console.log(`ERROR: This login attempt has failed! ${err.message}`);
|
||||
});
|
||||
|
||||
// Start our login attempt
|
||||
let startResult = await session.startWithCredentials({accountName, password});
|
||||
if (startResult.actionRequired) {
|
||||
// Some Steam Guard action is required. We only care about email and device codes; in theory an
|
||||
// EmailConfirmation and/or DeviceConfirmation action could be possible, but we're just going to ignore those.
|
||||
// If the user does receive a confirmation and accepts it, LoginSession will detect and handle that automatically.
|
||||
// The only consequence of ignoring it here is that we don't print a message to the user indicating that they
|
||||
// could accept an email or device confirmation.
|
||||
|
||||
let codeActionTypes = [SteamSession.EAuthSessionGuardType.EmailCode, SteamSession.EAuthSessionGuardType.DeviceCode];
|
||||
let codeAction = startResult.validActions.find(action => codeActionTypes.includes(action.type));
|
||||
if (codeAction) {
|
||||
if (codeAction.type == SteamSession.EAuthSessionGuardType.EmailCode) {
|
||||
// We wouldn't expect this to happen since we're trying to disable 2FA, but just in case...
|
||||
console.log(`A code has been sent to your email address at ${codeAction.detail}.`);
|
||||
} else {
|
||||
console.log('You need to provide a Steam Guard Mobile Authenticator code.');
|
||||
}
|
||||
|
||||
let code = await promptAsync('Code: ');
|
||||
if (code) {
|
||||
await session.submitSteamGuardCode(code);
|
||||
}
|
||||
|
||||
// If we fall through here without submitting a Steam Guard code, that means one of two things:
|
||||
// 1. The user pressed enter without providing a code, in which case the script will simply exit
|
||||
// 2. The user approved a device/email confirmation, in which case 'authenticated' was emitted and the prompt was canceled
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function doRevoke() {
|
||||
let rCode = await promptAsync('Revocation Code: R');
|
||||
community.disableTwoFactor('R' + rCode, (err) => {
|
||||
if (err) {
|
||||
console.log(err);
|
||||
process.exit();
|
||||
return;
|
||||
}
|
||||
|
||||
console.log('Two-factor authentication disabled!');
|
||||
process.exit();
|
||||
});
|
||||
}
|
||||
|
||||
// Nothing interesting below here, just code for prompting for input from the console.
|
||||
|
||||
function promptAsync(question, sensitiveInput = false) {
|
||||
return new Promise((resolve) => {
|
||||
let rl = ReadLine.createInterface({
|
||||
input: process.stdin,
|
||||
output: sensitiveInput ? null : process.stdout,
|
||||
terminal: true
|
||||
});
|
||||
|
||||
g_AbortPromptFunc = () => {
|
||||
rl.close();
|
||||
resolve('');
|
||||
};
|
||||
|
||||
if (sensitiveInput) {
|
||||
// We have to write the question manually if we didn't give readline an output stream
|
||||
process.stdout.write(question);
|
||||
}
|
||||
|
||||
rl.question(question, (result) => {
|
||||
if (sensitiveInput) {
|
||||
// We have to manually print a newline
|
||||
process.stdout.write('\n');
|
||||
}
|
||||
|
||||
g_AbortPromptFunc = null;
|
||||
rl.close();
|
||||
resolve(result);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function abortPrompt() {
|
||||
if (!g_AbortPromptFunc) {
|
||||
return;
|
||||
}
|
||||
|
||||
g_AbortPromptFunc();
|
||||
process.stdout.write('\n');
|
||||
}
|
||||
118
examples/edit-group-announcement.js
Normal file
118
examples/edit-group-announcement.js
Normal file
@@ -0,0 +1,118 @@
|
||||
let SteamCommunity = require('../index.js');
|
||||
let ReadLine = require('readline');
|
||||
|
||||
let community = new SteamCommunity();
|
||||
let rl = ReadLine.createInterface({
|
||||
input: process.stdin,
|
||||
output: process.stdout
|
||||
});
|
||||
|
||||
rl.question('Username: ', function(accountName) {
|
||||
rl.question('Password: ', function(password) {
|
||||
doLogin(accountName, password);
|
||||
});
|
||||
});
|
||||
|
||||
function doLogin(accountName, password, authCode, twoFactorCode, captcha) {
|
||||
community.login({
|
||||
accountName: accountName,
|
||||
password: password,
|
||||
authCode: authCode,
|
||||
twoFactorCode: twoFactorCode,
|
||||
captcha: captcha
|
||||
}, function(err, sessionID, cookies, steamguard) {
|
||||
if (err) {
|
||||
if (err.message == 'SteamGuardMobile') {
|
||||
rl.question('Steam Authenticator Code: ', function(code) {
|
||||
doLogin(accountName, password, null, code);
|
||||
});
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if (err.message == 'SteamGuard') {
|
||||
console.log('An email has been sent to your address at ' + err.emaildomain);
|
||||
rl.question('Steam Guard Code: ', function(code) {
|
||||
doLogin(accountName, password, code);
|
||||
});
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if (err.message == 'CAPTCHA') {
|
||||
console.log(err.captchaurl);
|
||||
rl.question('CAPTCHA: ', function(captchaInput) {
|
||||
doLogin(accountName, password, authCode, twoFactorCode, captchaInput);
|
||||
});
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
console.log(err);
|
||||
process.exit();
|
||||
return;
|
||||
}
|
||||
|
||||
console.log('Logged on!');
|
||||
|
||||
rl.question('Group ID: ', function(gid) {
|
||||
community.getSteamGroup(gid, function(err, group) {
|
||||
if (err) {
|
||||
console.log(err);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
group.getAllAnnouncements(function(err, announcements) {
|
||||
|
||||
if (announcements.length === 0) {
|
||||
return console.log('This group has no announcements');
|
||||
}
|
||||
|
||||
for (let i = announcements.length - 1; i >= 0; i--) {
|
||||
console.log('[%s] %s %s: %s', announcements[i].date, announcements[i].aid, announcements[i].author, announcements[i].content);
|
||||
}
|
||||
|
||||
rl.question('Would you like to delete delete or edit an annoucement? (Type edit/delete): ', function(choice) {
|
||||
rl.question('Annoucement ID: ', function(aid) {
|
||||
if (choice === 'edit') {
|
||||
rl.question('New title: ', function(header) {
|
||||
rl.question('New body: ', function(content) {
|
||||
// EW THE PYRAMID!
|
||||
// Try replace this with delete!
|
||||
editAnnouncement(group, aid, header, content);
|
||||
});
|
||||
});
|
||||
} else {
|
||||
deleteAnnouncement(group, aid);
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function editAnnouncement(group, aid, header, content) {
|
||||
// Actual community method.
|
||||
group.editAnnouncement(aid, header, content, function(error) {
|
||||
if (!error) {
|
||||
console.log('Annoucement edited!');
|
||||
} else {
|
||||
console.log('Unable to edit annoucement! %j', error);
|
||||
process.exit(1);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function deleteAnnouncement(group, aid) {
|
||||
// group.deleteAnnouncement(aid);
|
||||
// Or
|
||||
group.deleteAnnouncement(aid, function(err) {
|
||||
if (!err) {
|
||||
console.log('Deleted');
|
||||
} else {
|
||||
console.log('Error deleting announcement.');
|
||||
}
|
||||
});
|
||||
}
|
||||
182
examples/enable_twofactor.js
Normal file
182
examples/enable_twofactor.js
Normal file
@@ -0,0 +1,182 @@
|
||||
// If you aren't running this script inside of the repository, replace the following line with:
|
||||
// const SteamCommunity = require('steamcommunity');
|
||||
const SteamCommunity = require('../index.js');
|
||||
const SteamSession = require('steam-session');
|
||||
const ReadLine = require('readline');
|
||||
const FS = require('fs');
|
||||
|
||||
const EResult = SteamCommunity.EResult;
|
||||
|
||||
let g_AbortPromptFunc = null;
|
||||
|
||||
let community = new SteamCommunity();
|
||||
|
||||
main();
|
||||
async function main() {
|
||||
let accountName = await promptAsync('Username: ');
|
||||
let password = await promptAsync('Password (hidden): ', true);
|
||||
|
||||
// Create a LoginSession for us to use to attempt to log into steam
|
||||
let session = new SteamSession.LoginSession(SteamSession.EAuthTokenPlatformType.MobileApp);
|
||||
|
||||
// Go ahead and attach our event handlers before we do anything else.
|
||||
session.on('authenticated', async () => {
|
||||
abortPrompt();
|
||||
|
||||
let accessToken = session.accessToken;
|
||||
let cookies = await session.getWebCookies();
|
||||
|
||||
community.setCookies(cookies);
|
||||
community.setMobileAppAccessToken(accessToken);
|
||||
|
||||
// Enabling or disabling 2FA is presently the only action in node-steamcommunity which requires an access token.
|
||||
// In all other cases, using `community.setCookies(cookies)` is all you need to do in order to be logged in,
|
||||
// although there's never any harm in setting a mobile app access token.
|
||||
|
||||
doSetup();
|
||||
});
|
||||
|
||||
session.on('timeout', () => {
|
||||
abortPrompt();
|
||||
console.log('This login attempt has timed out.');
|
||||
});
|
||||
|
||||
session.on('error', (err) => {
|
||||
abortPrompt();
|
||||
|
||||
// This should ordinarily not happen. This only happens in case there's some kind of unexpected error while
|
||||
// polling, e.g. the network connection goes down or Steam chokes on something.
|
||||
|
||||
console.log(`ERROR: This login attempt has failed! ${err.message}`);
|
||||
});
|
||||
|
||||
// Start our login attempt
|
||||
let startResult = await session.startWithCredentials({accountName, password});
|
||||
if (startResult.actionRequired) {
|
||||
// Some Steam Guard action is required. We only care about email and device codes; in theory an
|
||||
// EmailConfirmation and/or DeviceConfirmation action could be possible, but we're just going to ignore those.
|
||||
// If the user does receive a confirmation and accepts it, LoginSession will detect and handle that automatically.
|
||||
// The only consequence of ignoring it here is that we don't print a message to the user indicating that they
|
||||
// could accept an email or device confirmation.
|
||||
|
||||
let codeActionTypes = [SteamSession.EAuthSessionGuardType.EmailCode, SteamSession.EAuthSessionGuardType.DeviceCode];
|
||||
let codeAction = startResult.validActions.find(action => codeActionTypes.includes(action.type));
|
||||
if (codeAction) {
|
||||
if (codeAction.type == SteamSession.EAuthSessionGuardType.EmailCode) {
|
||||
console.log(`A code has been sent to your email address at ${codeAction.detail}.`);
|
||||
} else {
|
||||
// We wouldn't expect this to happen since we're trying to enable 2FA, but just in case...
|
||||
console.log('You need to provide a Steam Guard Mobile Authenticator code.');
|
||||
}
|
||||
|
||||
let code = await promptAsync('Code: ');
|
||||
if (code) {
|
||||
await session.submitSteamGuardCode(code);
|
||||
}
|
||||
|
||||
// If we fall through here without submitting a Steam Guard code, that means one of two things:
|
||||
// 1. The user pressed enter without providing a code, in which case the script will simply exit
|
||||
// 2. The user approved a device/email confirmation, in which case 'authenticated' was emitted and the prompt was canceled
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function doSetup() {
|
||||
community.enableTwoFactor((err, response) => {
|
||||
if (err) {
|
||||
if (err.eresult == EResult.Fail) {
|
||||
console.log('Error: Failed to enable two-factor authentication. Do you have a phone number attached to your account?');
|
||||
process.exit();
|
||||
return;
|
||||
}
|
||||
|
||||
if (err.eresult == EResult.RateLimitExceeded) {
|
||||
console.log('Error: RateLimitExceeded. Try again later.');
|
||||
process.exit();
|
||||
return;
|
||||
}
|
||||
|
||||
console.log(err);
|
||||
process.exit();
|
||||
return;
|
||||
}
|
||||
|
||||
if (response.status != EResult.OK) {
|
||||
console.log(`Error: Status ${response.status}`);
|
||||
process.exit();
|
||||
return;
|
||||
}
|
||||
|
||||
let filename = `twofactor_${community.steamID.getSteamID64()}.json`;
|
||||
console.log(`Writing secrets to ${filename}`);
|
||||
console.log(`Revocation code: ${response.revocation_code}`);
|
||||
FS.writeFileSync(filename, JSON.stringify(response, null, '\t'));
|
||||
|
||||
promptActivationCode(response);
|
||||
});
|
||||
}
|
||||
|
||||
async function promptActivationCode(response) {
|
||||
if (response.phone_number_hint) {
|
||||
console.log(`A code has been sent to your phone ending in ${response.phone_number_hint}.`);
|
||||
}
|
||||
|
||||
let smsCode = await promptAsync('SMS Code: ');
|
||||
community.finalizeTwoFactor(response.shared_secret, smsCode, (err) => {
|
||||
if (err) {
|
||||
if (err.message == 'Invalid activation code') {
|
||||
console.log(err);
|
||||
promptActivationCode(response);
|
||||
return;
|
||||
}
|
||||
|
||||
console.log(err);
|
||||
} else {
|
||||
console.log('Two-factor authentication enabled!');
|
||||
}
|
||||
|
||||
process.exit();
|
||||
});
|
||||
}
|
||||
|
||||
// Nothing interesting below here, just code for prompting for input from the console.
|
||||
|
||||
function promptAsync(question, sensitiveInput = false) {
|
||||
return new Promise((resolve) => {
|
||||
let rl = ReadLine.createInterface({
|
||||
input: process.stdin,
|
||||
output: sensitiveInput ? null : process.stdout,
|
||||
terminal: true
|
||||
});
|
||||
|
||||
g_AbortPromptFunc = () => {
|
||||
rl.close();
|
||||
resolve('');
|
||||
};
|
||||
|
||||
if (sensitiveInput) {
|
||||
// We have to write the question manually if we didn't give readline an output stream
|
||||
process.stdout.write(question);
|
||||
}
|
||||
|
||||
rl.question(question, (result) => {
|
||||
if (sensitiveInput) {
|
||||
// We have to manually print a newline
|
||||
process.stdout.write('\n');
|
||||
}
|
||||
|
||||
g_AbortPromptFunc = null;
|
||||
rl.close();
|
||||
resolve(result);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function abortPrompt() {
|
||||
if (!g_AbortPromptFunc) {
|
||||
return;
|
||||
}
|
||||
|
||||
g_AbortPromptFunc();
|
||||
process.stdout.write('\n');
|
||||
}
|
||||
715
index.js
715
index.js
@@ -1,312 +1,507 @@
|
||||
var Request = require('request');
|
||||
var RSA = require('node-bignumber').Key;
|
||||
var hex2b64 = require('node-bignumber').hex2b64;
|
||||
var SteamID = require('steamid');
|
||||
const {EventEmitter} = require('events');
|
||||
const StdLib = require('@doctormckay/stdlib');
|
||||
const SteamID = require('steamid');
|
||||
const {LoginSession, EAuthTokenPlatformType, EAuthSessionGuardType} = require('steam-session');
|
||||
const Util = require('util');
|
||||
const xml2js = require('xml2js');
|
||||
|
||||
require('util').inherits(SteamCommunity, require('events').EventEmitter);
|
||||
const Helpers = require('./components/helpers.js');
|
||||
const Package = require('./package.json');
|
||||
|
||||
const USER_AGENT = 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/114.0.0.0 Safari/537.36';
|
||||
|
||||
Util.inherits(SteamCommunity, EventEmitter);
|
||||
|
||||
module.exports = SteamCommunity;
|
||||
|
||||
SteamCommunity.SteamID = SteamID;
|
||||
SteamCommunity.EConfirmationType = require('./resources/EConfirmationType.js');
|
||||
SteamCommunity.EResult = require('./resources/EResult.js');
|
||||
SteamCommunity.ESharedFileType = require('./resources/ESharedFileType.js');
|
||||
SteamCommunity.EFriendRelationship = require('./resources/EFriendRelationship.js');
|
||||
|
||||
function SteamCommunity() {
|
||||
this._jar = Request.jar();
|
||||
/**
|
||||
*
|
||||
* @param {object} [options]
|
||||
* @param {number} [options.timeout=50000] - The time in milliseconds that SteamCommunity will wait for HTTP requests to complete.
|
||||
* @param {string} [options.localAddress] - The local IP address that SteamCommunity will use for its HTTP requests.
|
||||
* @param {string} [options.httpProxy] - A string containing the URI of an HTTP proxy to use for all requests, e.g. `http://user:pass@1.2.3.4:8888`
|
||||
* @param {object} [options.defaultHttpHeaders] - An object containing some headers to send for every HTTP request
|
||||
* @constructor
|
||||
*/
|
||||
function SteamCommunity(options) {
|
||||
options = options || {};
|
||||
|
||||
this.packageName = Package.name;
|
||||
this.packageVersion = Package.version;
|
||||
|
||||
this._jar = new StdLib.HTTP.CookieJar();
|
||||
this._captchaGid = -1;
|
||||
this.request = Request.defaults({"jar": this._jar});
|
||||
this.chatState = SteamCommunity.ChatState.Offline;
|
||||
|
||||
this._httpRequestID = 0;
|
||||
|
||||
let defaultHeaders = {
|
||||
'user-agent': USER_AGENT
|
||||
};
|
||||
|
||||
// Apply the user's custom default headers
|
||||
for (let i in (options.defaultHttpHeaders || {})) {
|
||||
// Make sure all header names are lower case to avoid conflicts
|
||||
defaultHeaders[i.toLowerCase()] = options.defaultHttpHeaders[i];
|
||||
}
|
||||
|
||||
this._httpClient = new StdLib.HTTP.HttpClient({
|
||||
httpAgent: options.httpProxy ? StdLib.HTTP.getProxyAgent(false, options.httpProxy) : null,
|
||||
httpsAgent: options.httpProxy ? StdLib.HTTP.getProxyAgent(true, options.httpProxy) : null,
|
||||
localAddress: options.localAddress,
|
||||
defaultHeaders,
|
||||
defaultTimeout: options.timeout || 50000,
|
||||
cookieJar: this._jar,
|
||||
gzip: true
|
||||
});
|
||||
|
||||
this._options = options;
|
||||
|
||||
// English
|
||||
this._jar.setCookie(Request.cookie('Steam_Language=english'), 'https://steamcommunity.com');
|
||||
this._setCookie('Steam_Language=english');
|
||||
|
||||
// UTC
|
||||
this._setCookie('timezoneOffset=0,0');
|
||||
}
|
||||
|
||||
SteamCommunity.prototype.login = function(details, callback) {
|
||||
if(details.steamguard) {
|
||||
var parts = details.steamguard.split('||');
|
||||
this._jar.setCookie(Request.cookie('steamMachineAuth' + parts[0] + '=' + encodeURIComponent(parts[1])), 'https://steamcommunity.com');
|
||||
/**
|
||||
* @param {object} details
|
||||
* @param {string} details.accountName
|
||||
* @param {string} details.password
|
||||
* @param {string} [details.authCode]
|
||||
* @param {string} [details.twoFactorCode]
|
||||
* @param {string} [details.authTokenPlatformType] - A value from steam-session's EAuthTokenPlatformType enum. Defaults to MobileApp.
|
||||
* @return Promise<{cookies: string[], sessionID: string, refreshToken: string}>
|
||||
*/
|
||||
SteamCommunity.prototype.login = function(details) {
|
||||
if (typeof details.accountName != 'string' || typeof details.password != 'string') {
|
||||
throw new Error('You must provide your accountName and password to login to steamcommunity.com');
|
||||
}
|
||||
|
||||
var self = this;
|
||||
this.request.post("https://steamcommunity.com/login/getrsakey/", {"form": {"username": details.accountName}}, function(err, response, body) {
|
||||
if(err) {
|
||||
callback(err);
|
||||
return;
|
||||
}
|
||||
|
||||
var json;
|
||||
try {
|
||||
json = JSON.parse(body);
|
||||
} catch(e) {
|
||||
callback(e);
|
||||
return;
|
||||
}
|
||||
|
||||
var key = new RSA();
|
||||
key.setPublic(json.publickey_mod, json.publickey_exp);
|
||||
|
||||
var form = {
|
||||
"captcha_text": details.captcha || "",
|
||||
"captchagid": self._captchaGid,
|
||||
"emailauth": details.authCode || "",
|
||||
"emailsteamid": "",
|
||||
"loginfriendlyname": "",
|
||||
"password": hex2b64(key.encrypt(details.password)),
|
||||
"remember_login": "true",
|
||||
"rsatimestamp": json.timestamp,
|
||||
"twofactorcode": "",
|
||||
"username": details.accountName
|
||||
};
|
||||
|
||||
self.request.post({
|
||||
"uri": "https://steamcommunity.com/login/dologin/",
|
||||
"json": true,
|
||||
"form": form
|
||||
}, function(err, response, body) {
|
||||
if(self._checkHttpError(err, response, callback)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if(!body.success && body.emailauth_needed) {
|
||||
var error = new Error("SteamGuard");
|
||||
error.emaildomain = body.emaildomain;
|
||||
|
||||
callback(error);
|
||||
} else if(!body.success && body.captcha_needed) {
|
||||
var error = new Error("CAPTCHA");
|
||||
error.captchaurl = "https://steamcommunity.com/public/captcha.php?gid=" + body.captcha_gid;
|
||||
|
||||
self._captchaGid = body.captcha_gid;
|
||||
|
||||
callback(error);
|
||||
} else if(!body.success) {
|
||||
callback(new Error(body.message || "Unknown error"));
|
||||
} else {
|
||||
var sessionID = generateSessionID();
|
||||
self._jar.setCookie(Request.cookie('sessionid=' + sessionID), 'http://steamcommunity.com');
|
||||
|
||||
self.steamID = new SteamID(body.transfer_parameters.steamid);
|
||||
var cookies = self._jar.getCookieString("https://steamcommunity.com").split(';').map(function(cookie) {
|
||||
return cookie.trim();
|
||||
});
|
||||
|
||||
// Find the Steam Guard cookie
|
||||
var steamguard = null;
|
||||
for(var i = 0; i < cookies.length; i++) {
|
||||
var parts = cookies[i].split('=');
|
||||
if(parts[0] == 'steamMachineAuth' + self.steamID) {
|
||||
steamguard = self.steamID.toString() + '||' + decodeURIComponent(parts[1]);
|
||||
break;
|
||||
}
|
||||
|
||||
// eslint-disable-next-line no-async-promise-executor
|
||||
return new Promise(async (resolve, reject) => {
|
||||
let platformType = details.authTokenPlatformType || EAuthTokenPlatformType.MobileApp;
|
||||
let session = new LoginSession(platformType, {
|
||||
httpProxy: this._options.httpProxy
|
||||
});
|
||||
|
||||
session.on('authenticated', async () => {
|
||||
try {
|
||||
let cookies = await session.getWebCookies();
|
||||
this.setCookies(cookies);
|
||||
|
||||
if (platformType == EAuthTokenPlatformType.MobileApp) {
|
||||
this.setMobileAppAccessToken(session.accessToken);
|
||||
}
|
||||
|
||||
callback(null, sessionID, cookies, steamguard);
|
||||
|
||||
// TODO set refresh token for session keep-alive
|
||||
|
||||
let sessionID = this.getSessionID();
|
||||
if (!cookies.some(c => c.startsWith('sessionid='))) {
|
||||
// make sure that the sessionid we return is in the cookies list we return
|
||||
cookies.push(`sessionid=${sessionID}`);
|
||||
}
|
||||
|
||||
resolve({
|
||||
cookies,
|
||||
sessionID,
|
||||
refreshToken: session.refreshToken
|
||||
});
|
||||
} catch (ex) {
|
||||
reject(ex);
|
||||
}
|
||||
});
|
||||
|
||||
session.on('timeout', () => {
|
||||
// This really shouldn't happen
|
||||
reject(new Error('Login attempt timed out'));
|
||||
});
|
||||
session.on('error', reject);
|
||||
|
||||
try {
|
||||
let startResult = await session.startWithCredentials({
|
||||
accountName: details.accountName,
|
||||
password: details.password,
|
||||
steamGuardCode: details.twoFactorCode || details.authCode
|
||||
});
|
||||
|
||||
if (!startResult.actionRequired) {
|
||||
return; // 'authenticated' should get emitted soon
|
||||
}
|
||||
|
||||
session.cancelLoginAttempt();
|
||||
|
||||
if (startResult.validActions.some(a => a.type == EAuthSessionGuardType.EmailCode)) {
|
||||
return reject(new Error('SteamGuard'));
|
||||
}
|
||||
|
||||
if (startResult.validActions.some(a => a.type == EAuthSessionGuardType.DeviceCode)) {
|
||||
return reject(new Error('SteamGuardMobile'));
|
||||
}
|
||||
|
||||
let validActions = startResult.validActions.map(a => a.type).join(', ');
|
||||
return reject(new Error(`Unexpected guard action(s) ${validActions}`));
|
||||
} catch (ex) {
|
||||
reject(ex);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Get a token that can be used to log onto Steam using steam-user.
|
||||
* @param {function} [callback]
|
||||
* @return Promise<{steamID: SteamID, accountName: string, webLogonToken: string}>
|
||||
*/
|
||||
SteamCommunity.prototype.getClientLogonToken = function(callback) {
|
||||
return StdLib.Promises.callbackPromise(null, callback, false, async (resolve, reject) => {
|
||||
let {jsonBody} = await this.httpRequest({
|
||||
method: 'GET',
|
||||
url: 'https://steamcommunity.com/chat/clientjstoken',
|
||||
source: 'steamcommunity'
|
||||
});
|
||||
|
||||
if (!jsonBody.logged_in) {
|
||||
let e = new Error('Not Logged In');
|
||||
this._notifySessionExpired(e);
|
||||
return reject(e);
|
||||
}
|
||||
|
||||
if (!jsonBody.steamid || !jsonBody.account_name || !jsonBody.token) {
|
||||
return reject(new Error('Malformed response'));
|
||||
}
|
||||
|
||||
resolve({
|
||||
steamID: new SteamID(jsonBody.steamid),
|
||||
accountName: jsonBody.account_name,
|
||||
webLogonToken: jsonBody.token
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
SteamCommunity.prototype.setCookies = function(cookies) {
|
||||
var self = this;
|
||||
cookies.forEach(function(cookie) {
|
||||
var cookieName = cookie.match(/(.+)=/)[1];
|
||||
if(cookieName == 'steamLogin') {
|
||||
self.steamID = new SteamID(cookie.match(/=(\d+)/)[1]);
|
||||
}
|
||||
|
||||
self._jar.setCookie(Request.cookie(cookie), (cookieName.match(/^steamMachineAuth/) || cookieName.match(/Secure$/) ? "https://" : "http://") + "steamcommunity.com");
|
||||
});
|
||||
/**
|
||||
* Sets a single cookie in our cookie jar.
|
||||
* @param {string} cookie
|
||||
* @private
|
||||
*/
|
||||
SteamCommunity.prototype._setCookie = function(cookie) {
|
||||
this._jar.add(cookie, 'steamcommunity.com');
|
||||
this._jar.add(cookie, 'store.steampowered.com');
|
||||
this._jar.add(cookie, 'help.steampowered.com');
|
||||
};
|
||||
|
||||
SteamCommunity.prototype.getSessionID = function() {
|
||||
var cookies = this._jar.getCookieString("http://steamcommunity.com").split(';');
|
||||
for(var i = 0; i < cookies.length; i++) {
|
||||
var match = cookies[i].trim().match(/([^=]+)=(.+)/);
|
||||
if(match[1] == 'sessionid') {
|
||||
return decodeURIComponent(match[2]);
|
||||
}
|
||||
/**
|
||||
* Set one or more cookies in this SteamCommunity's cookie jar.
|
||||
* @param {string|string[]} cookies
|
||||
*/
|
||||
SteamCommunity.prototype.setCookies = function(cookies) {
|
||||
if (!Array.isArray(cookies)) {
|
||||
cookies = [cookies];
|
||||
}
|
||||
|
||||
var sessionID = generateSessionID();
|
||||
this._jar.setCookie(Request.cookie('sessionid=' + sessionID), "http://steamcommunity.com");
|
||||
|
||||
cookies.forEach((cookie) => {
|
||||
let cookieName = cookie.match(/(.+)=/)[1];
|
||||
if (cookieName == 'steamLogin' || cookieName == 'steamLoginSecure') {
|
||||
this.steamID = new SteamID(cookie.match(/=(\d+)/)[1]);
|
||||
}
|
||||
|
||||
this._setCookie(cookie);
|
||||
});
|
||||
|
||||
// The account we're logged in as might have changed, so verify that our mobile access token (if any) is still valid
|
||||
// for this account.
|
||||
this._verifyMobileAccessToken();
|
||||
};
|
||||
|
||||
SteamCommunity.prototype.getSessionID = function(domain = 'steamcommunity.com') {
|
||||
let sessionIdCookie = this._jar.cookies
|
||||
.filter(c => c.domain == domain)
|
||||
.find(c => c.name == 'sessionid');
|
||||
if (sessionIdCookie) {
|
||||
return sessionIdCookie.content;
|
||||
}
|
||||
|
||||
// No cookie found? Generate a new session id
|
||||
let sessionID = require('crypto').randomBytes(12).toString('hex');
|
||||
this._setCookie(`sessionid=${sessionID}`);
|
||||
return sessionID;
|
||||
};
|
||||
|
||||
function generateSessionID() {
|
||||
return Math.floor(Math.random() * 1000000000);
|
||||
};
|
||||
|
||||
SteamCommunity.prototype.getWebApiKey = function(domain, callback) {
|
||||
var self = this;
|
||||
this.request("https://steamcommunity.com/dev/apikey", function(err, response, body) {
|
||||
if(self._checkHttpError(err, response, callback)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if(body.match(/<h2>Access Denied<\/h2>/)) {
|
||||
return callback("Access Denied");
|
||||
}
|
||||
|
||||
var match = body.match(/<p>Key: ([0-9A-F]+)<\/p>/);
|
||||
if(match) {
|
||||
// We already have an API key registered
|
||||
callback(null, match[1]);
|
||||
} else {
|
||||
// We need to register a new API key
|
||||
self.request.post('https://steamcommunity.com/dev/registerkey', {
|
||||
"form": {
|
||||
"domain": domain,
|
||||
"agreeToTerms": "agreed",
|
||||
"sessionid": self.getSessionID(),
|
||||
"Submit": "Register"
|
||||
}
|
||||
}, function(err, response, body) {
|
||||
if(err || response.statusCode >= 400) {
|
||||
return callback(err.message || "HTTP error " + response.statusCode);
|
||||
}
|
||||
|
||||
self.getWebApiKey(domain, callback);
|
||||
});
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* @param {string} pin
|
||||
* @param {function} [callback]
|
||||
* @return Promise<void>
|
||||
*/
|
||||
SteamCommunity.prototype.parentalUnlock = function(pin, callback) {
|
||||
this.request.post("https://steamcommunity.com/parental/ajaxunlock", {
|
||||
"json": true,
|
||||
"form": {
|
||||
"pin": pin
|
||||
}
|
||||
}, function(err, response, body) {
|
||||
if(!callback) {
|
||||
return;
|
||||
}
|
||||
|
||||
if(self._checkHttpError(err, response, callback)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if(!body || typeof body.success !== 'boolean') {
|
||||
return callback("Invalid response");
|
||||
}
|
||||
|
||||
if(!body.success) {
|
||||
return callback("Incorrect PIN");
|
||||
}
|
||||
|
||||
callback();
|
||||
}.bind(this));
|
||||
};
|
||||
let sessionID = this.getSessionID();
|
||||
|
||||
SteamCommunity.prototype.getNotifications = function(callback) {
|
||||
this.request.get("https://steamcommunity.com/actions/RefreshNotificationArea", function(err, response, body) {
|
||||
if(self._checkHttpError(err, response, callback)) {
|
||||
return;
|
||||
return StdLib.Promises.callbackPromise(null, callback, true, async (resolve, reject) => {
|
||||
let {jsonBody} = await this.httpRequest({
|
||||
method: 'POST',
|
||||
url: 'https://steamcommunity.com/parental/ajaxunlock',
|
||||
form: {
|
||||
pin: pin,
|
||||
sessionid: sessionID
|
||||
},
|
||||
source: 'steamcommunity'
|
||||
});
|
||||
|
||||
if (!jsonBody || typeof jsonBody.success !== 'boolean') {
|
||||
return reject('Invalid response');
|
||||
}
|
||||
|
||||
var notifications = {
|
||||
"comments": 0,
|
||||
"items": 0,
|
||||
"invites": 0,
|
||||
"gifts": 0,
|
||||
"chat": 0,
|
||||
"trades": 0
|
||||
};
|
||||
|
||||
var items = {
|
||||
"comments": /(\d+) new comments?/,
|
||||
"items": /(\d+) new items? in your inventory/,
|
||||
"invites": /(\d+) new invites?/,
|
||||
"gifts": /(\d+) new gifts?/,
|
||||
"chat": /(\d+) unread chat messages?/,
|
||||
"trades": /(\d+) new trade notifications?/
|
||||
};
|
||||
|
||||
var match;
|
||||
for(var i in items) {
|
||||
if(match = body.match(items[i])) {
|
||||
notifications[i] = parseInt(match[1], 10);
|
||||
|
||||
if (!jsonBody.success) {
|
||||
switch (jsonBody.eresult) {
|
||||
case SteamCommunity.EResult.AccessDenied:
|
||||
return reject('Incorrect PIN');
|
||||
|
||||
case SteamCommunity.EResult.LimitExceeded:
|
||||
return reject('Too many invalid PIN attempts');
|
||||
|
||||
default:
|
||||
return reject('Error ' + jsonBody.eresult);
|
||||
}
|
||||
}
|
||||
|
||||
callback(null, notifications);
|
||||
|
||||
resolve();
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* @param {function} [callback]
|
||||
* @return Promise<object>
|
||||
*/
|
||||
SteamCommunity.prototype.getNotifications = function(callback) {
|
||||
return StdLib.Promises.callbackPromise(null, callback, false, async (resolve, reject) => {
|
||||
let {jsonBody} = await this.httpRequest({
|
||||
method: 'GET',
|
||||
url: 'https://steamcommunity.com/actions/GetNotificationCounts',
|
||||
source: 'steamcommunity'
|
||||
});
|
||||
|
||||
if (!jsonBody || !jsonBody.notifications) {
|
||||
return reject(new Error('Malformed response'));
|
||||
}
|
||||
|
||||
let notifications = {
|
||||
trades: jsonBody.notifications[1] || 0,
|
||||
gameTurns: jsonBody.notifications[2] || 0,
|
||||
moderatorMessages: jsonBody.notifications[3] || 0,
|
||||
comments: jsonBody.notifications[4] || 0,
|
||||
items: jsonBody.notifications[5] || 0,
|
||||
invites: jsonBody.notifications[6] || 0,
|
||||
// dunno about 7
|
||||
gifts: jsonBody.notifications[8] || 0,
|
||||
chat: jsonBody.notifications[9] || 0,
|
||||
helpRequestReplies: jsonBody.notifications[10] || 0,
|
||||
accountAlerts: jsonBody.notifications[11] || 0
|
||||
};
|
||||
|
||||
resolve(notifications);
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* @param {function} [callback]
|
||||
* @return Promise<void>
|
||||
*/
|
||||
SteamCommunity.prototype.resetItemNotifications = function(callback) {
|
||||
this.request.get("https://steamcommunity.com/my/inventory", function(err, response, body) {
|
||||
if(!callback) {
|
||||
return;
|
||||
}
|
||||
|
||||
if(self._checkHttpError(err, response, callback)) {
|
||||
return;
|
||||
}
|
||||
|
||||
callback(null);
|
||||
return StdLib.Promises.callbackPromise(null, callback, true, async (resolve, reject) => {
|
||||
await this.httpRequest({
|
||||
method: 'GET',
|
||||
url: 'https://steamcommunity.com/my/inventory',
|
||||
source: 'steamcommunity'
|
||||
});
|
||||
|
||||
resolve();
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* @param {function} [callback]
|
||||
* @return Promise<{loggedIn: boolean, familyView: boolean}>
|
||||
*/
|
||||
SteamCommunity.prototype.loggedIn = function(callback) {
|
||||
this.request("https://steamcommunity.com/my", {"followRedirect": false}, function(err, response, body) {
|
||||
if(err || (response.statusCode != 302 && response.statusCode != 403)) {
|
||||
callback(err || new Error("HTTP error " + response.statusCode));
|
||||
return;
|
||||
return StdLib.Promises.callbackPromise(['loggedIn', 'familyView'], callback, false, async (resolve, reject) => {
|
||||
let result = await this.httpRequest({
|
||||
method: 'GET',
|
||||
url: 'https://steamcommunity.com/my',
|
||||
followRedirect: false,
|
||||
checkHttpError: false,
|
||||
source: 'steamcommunity'
|
||||
});
|
||||
|
||||
if (result.statusCode != 302 && result.statusCode != 403) {
|
||||
return reject(new Error(`HTTP error ${result.statusCode}`));
|
||||
}
|
||||
|
||||
if(response.statusCode == 403) {
|
||||
callback(null, true, true);
|
||||
return;
|
||||
|
||||
if (result.statusCode == 403) {
|
||||
// TODO check response body to see if this is an akamai block
|
||||
return resolve({
|
||||
loggedIn: true,
|
||||
familyView: true
|
||||
});
|
||||
}
|
||||
|
||||
callback(null, !!response.headers.location.match(/steamcommunity\.com(\/(id|profiles)\/[^\/]+)\/?/), false);
|
||||
|
||||
return resolve({
|
||||
loggedIn: !!result.headers.location.match(/steamcommunity\.com(\/(id|profiles)\/[^/]+)\/?/),
|
||||
familyView: false
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
SteamCommunity.prototype._checkCommunityError = function(html, callback) {
|
||||
if(html.match(/<h1>Sorry!<\/h1>/)) {
|
||||
var match = html.match(/<h3>(.+)<\/h3>/);
|
||||
callback(new Error(match ? match[1] : "Unknown error occurred"));
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
};
|
||||
/**
|
||||
* @param {function} [callback]
|
||||
* @return Promise<{url: string, token: string}>
|
||||
*/
|
||||
SteamCommunity.prototype.getTradeURL = function(callback) {
|
||||
return StdLib.Promises.callbackPromise(['url', 'token'], callback, false, async (resolve, reject) => {
|
||||
let {textBody} = await this._myProfile('tradeoffers/privacy');
|
||||
|
||||
SteamCommunity.prototype._myProfile = function(endpoint, form, callback) {
|
||||
var self = this;
|
||||
this.request("https://steamcommunity.com/my", {"followRedirect": false}, function(err, response, body) {
|
||||
if(err || response.statusCode != 302) {
|
||||
callback(err || "HTTP error " + response.statusCode);
|
||||
return;
|
||||
let match = textBody.match(/https?:\/\/(www.)?steamcommunity.com\/tradeoffer\/new\/?\?partner=\d+(&|&)token=([a-zA-Z0-9-_]+)/);
|
||||
if (!match) {
|
||||
return reject(new Error('Malformed response'));
|
||||
}
|
||||
|
||||
var match = response.headers.location.match(/steamcommunity\.com(\/(id|profiles)\/[^\/]+)\/?/);
|
||||
if(!match) {
|
||||
callback("Can't get profile URL");
|
||||
return;
|
||||
}
|
||||
|
||||
(form ? self.request.post : self.request)("https://steamcommunity.com" + match[1] + "/" + endpoint, form ? {"form": form} : {}, callback);
|
||||
|
||||
let token = match[3];
|
||||
resolve({
|
||||
url: match[0],
|
||||
token
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
SteamCommunity.prototype._checkHttpError = function(err, response, callback) {
|
||||
if(err) {
|
||||
callback(err);
|
||||
return true;
|
||||
}
|
||||
|
||||
if(response.statusCode != 200) {
|
||||
var error = new Error("HTTP error " + response.statusCode);
|
||||
error.code = response.statusCode;
|
||||
callback(error);
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
/**
|
||||
* @param [callback]
|
||||
* @return Promise<{url: string, token: string}>
|
||||
*/
|
||||
SteamCommunity.prototype.changeTradeURL = function(callback) {
|
||||
return StdLib.Promises.callbackPromise(['url', 'token'], callback, true, async (resolve, reject) => {
|
||||
let {textBody} = await this._myProfile('tradeoffers/newtradeurl', {sessionid: this.getSessionID()});
|
||||
|
||||
if (!textBody || typeof textBody !== 'string' || textBody.length < 3 || textBody.indexOf('"') !== 0) {
|
||||
return reject(new Error('Malformed response'));
|
||||
}
|
||||
|
||||
let newToken = textBody.replace(/"/g, ''); //"t1o2k3e4n" => t1o2k3e4n
|
||||
resolve({
|
||||
url: `https://steamcommunity.com/tradeoffer/new/?partner=${this.steamID.accountid}&token=${newToken}`,
|
||||
token: newToken
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Clear your profile name (alias) history.
|
||||
* @param {function} [callback]
|
||||
* @return Promise<void>
|
||||
*/
|
||||
SteamCommunity.prototype.clearPersonaNameHistory = function(callback) {
|
||||
return StdLib.Promises.callbackPromise(null, callback, true, async (resolve, reject) => {
|
||||
let {statusCode, textBody} = await this._myProfile('ajaxclearaliashistory/', {sessionid: this.getSessionID()});
|
||||
|
||||
if (statusCode != 200) {
|
||||
return reject(new Error(`HTTP error ${statusCode}`));
|
||||
}
|
||||
|
||||
try {
|
||||
let body = JSON.parse(textBody);
|
||||
let err = Helpers.eresultError(body.success);
|
||||
return err ? reject(err) : resolve();
|
||||
} catch (ex) {
|
||||
return reject(new Error('Malformed response'));
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Returns an object whose keys are 64-bit SteamIDs, and whose values are values from the EFriendRelationship enum.
|
||||
* Therefore, you can deduce your friends or blocked list from this object.
|
||||
* @param {function} [callback]
|
||||
* @return Promise<object[]>
|
||||
*/
|
||||
SteamCommunity.prototype.getFriendsList = function(callback) {
|
||||
return StdLib.Promises.callbackPromise(['friends'], callback, false, async (resolve, reject) => {
|
||||
let {jsonBody} = await this.httpRequest({
|
||||
method: 'GET',
|
||||
url: 'https://steamcommunity.com/textfilter/ajaxgetfriendslist',
|
||||
source: 'steamcommunity'
|
||||
});
|
||||
|
||||
if (jsonBody.success != SteamCommunity.EResult.OK) {
|
||||
return reject(Helpers.eresultError(jsonBody.success));
|
||||
}
|
||||
|
||||
if (!jsonBody.friendslist || !jsonBody.friendslist.friends) {
|
||||
return reject(new Error('Malformed response'));
|
||||
}
|
||||
|
||||
const friends = {};
|
||||
jsonBody.friendslist.friends.forEach(friend => (friends[friend.ulfriendid] = friend.efriendrelationship));
|
||||
resolve({friends});
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* @param {string} url
|
||||
* @return Promise<{vanityURL: string, steamID: SteamID}>
|
||||
* @private
|
||||
*/
|
||||
SteamCommunity.prototype._resolveVanityURL = async function(url) {
|
||||
// Precede url param if only the vanity was provided
|
||||
if (!url.includes('steamcommunity.com')) {
|
||||
url = `https://steamcommunity.com/id/${url}`;
|
||||
}
|
||||
|
||||
// Make request to get XML data
|
||||
let {textBody} = await this._httpRequest({
|
||||
method: 'GET',
|
||||
url,
|
||||
source: 'steamcommunity'
|
||||
});
|
||||
|
||||
return await new Promise((resolve, reject) => {
|
||||
// Parse XML data returned from Steam into an object
|
||||
new xml2js.Parser().parseString(textBody, (err, parsed) => {
|
||||
if (err) {
|
||||
return reject(new Error('Couldn\'t parse XML response'));
|
||||
}
|
||||
|
||||
if (parsed.response && parsed.response.error) {
|
||||
return reject(new Error('Couldn\'t find Steam ID'));
|
||||
}
|
||||
|
||||
let steamID64 = parsed.profile.steamID64[0];
|
||||
let vanityURL = parsed.profile.customURL[0];
|
||||
|
||||
resolve({
|
||||
vanityURL,
|
||||
steamID: new SteamID(steamID64)
|
||||
});
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
require('./components/http.js');
|
||||
require('./components/profile.js');
|
||||
require('./components/market.js');
|
||||
require('./components/groups.js');
|
||||
require('./components/users.js');
|
||||
require('./components/sharedfiles.js');
|
||||
require('./components/webapi.js');
|
||||
require('./components/twofactor.js');
|
||||
require('./components/confirmations.js');
|
||||
require('./components/help.js');
|
||||
require('./classes/CMarketItem.js');
|
||||
require('./classes/CMarketSearchResult.js');
|
||||
require('./classes/CSteamGroup.js');
|
||||
require('./classes/CSteamSharedFile.js');
|
||||
require('./classes/CSteamUser.js');
|
||||
require('./components/chat.js');
|
||||
|
||||
/**
|
||||
@callback SteamCommunity~genericErrorCallback
|
||||
@param {Error|null} err - An Error object on failure, or null on success
|
||||
*/
|
||||
|
||||
37
package.json
37
package.json
@@ -1,22 +1,43 @@
|
||||
{
|
||||
"name": "steamcommunity",
|
||||
"version": "3.0.0",
|
||||
"version": "4.0.0-dev",
|
||||
"private": true,
|
||||
"description": "Provides an interface for logging into and interacting with the Steam Community website",
|
||||
"keywords": ["steam", "steam community"],
|
||||
"keywords": [
|
||||
"steam",
|
||||
"steam community"
|
||||
],
|
||||
"homepage": "https://github.com/DoctorMcKay/node-steamcommunity",
|
||||
"bugs": {
|
||||
"url": "https://github.com/DoctorMcKay/node-steamcommunity/issues"
|
||||
},
|
||||
"license": "MIT",
|
||||
"author": {
|
||||
"name": "Alex Corn",
|
||||
"email": "mckay@doctormckay.com",
|
||||
"url": "https://www.doctormckay.com"
|
||||
},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/DoctorMcKay/node-steamcommunity.git"
|
||||
},
|
||||
"dependencies": {
|
||||
"request": "^2.58.0",
|
||||
"node-bignumber": "^1.2.1",
|
||||
"steamid": "^0.3.0",
|
||||
"xml2js": "^0.4.9",
|
||||
"cheerio": "^0.19.0"
|
||||
"@doctormckay/stdlib": "^2.6.0",
|
||||
"cheerio": "0.22.0",
|
||||
"image-size": "^0.8.2",
|
||||
"steam-session": "^1.2.4",
|
||||
"steam-totp": "^2.1.0",
|
||||
"steamid": "^2.0.0",
|
||||
"tough-cookie": "^4.0.0",
|
||||
"xml2js": "^0.6.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=14.0.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"eslint": "^7.31.0"
|
||||
},
|
||||
"scripts": {
|
||||
"lint": "npx eslint . --ext .js,.jsx,.ts,.tsx"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
14
resources/EConfirmationType.js
Normal file
14
resources/EConfirmationType.js
Normal file
@@ -0,0 +1,14 @@
|
||||
/* eslint-disable */
|
||||
|
||||
/**
|
||||
* @enum EConfirmationType
|
||||
*/
|
||||
module.exports = {
|
||||
// 1 is unknown, possibly "Invalid"
|
||||
"Trade": 2,
|
||||
"MarketListing": 3,
|
||||
// 4 is opt-out or other like account confirmation?
|
||||
|
||||
"2": "Trade",
|
||||
"3": "MarketListing"
|
||||
};
|
||||
25
resources/EFriendRelationship.js
Normal file
25
resources/EFriendRelationship.js
Normal file
@@ -0,0 +1,25 @@
|
||||
/* eslint-disable */
|
||||
|
||||
/**
|
||||
* @enum EFriendRelationship
|
||||
*/
|
||||
module.exports = {
|
||||
"None": 0,
|
||||
"Blocked": 1,
|
||||
"RequestRecipient": 2,
|
||||
"Friend": 3,
|
||||
"RequestInitiator": 4,
|
||||
"Ignored": 5,
|
||||
"IgnoredFriend": 6,
|
||||
"SuggestedFriend": 7, // removed "was used by the original implementation of the facebook linking feature; but now unused."
|
||||
|
||||
// Value-to-name mapping for convenience
|
||||
"0": "None",
|
||||
"1": "Blocked",
|
||||
"2": "RequestRecipient",
|
||||
"3": "Friend",
|
||||
"4": "RequestInitiator",
|
||||
"5": "Ignored",
|
||||
"6": "IgnoredFriend",
|
||||
"7": "SuggestedFriend",
|
||||
};
|
||||
25
resources/EPersonaState.js
Normal file
25
resources/EPersonaState.js
Normal file
@@ -0,0 +1,25 @@
|
||||
/* eslint-disable */
|
||||
|
||||
/**
|
||||
* @enum EPersonaState
|
||||
*/
|
||||
module.exports = {
|
||||
"Offline": 0,
|
||||
"Online": 1,
|
||||
"Busy": 2,
|
||||
"Away": 3,
|
||||
"Snooze": 4,
|
||||
"LookingToTrade": 5,
|
||||
"LookingToPlay": 6,
|
||||
"Invisible": 7,
|
||||
|
||||
// Value-to-name mapping for convenience
|
||||
"0": "Offline",
|
||||
"1": "Online",
|
||||
"2": "Busy",
|
||||
"3": "Away",
|
||||
"4": "Snooze",
|
||||
"5": "LookingToTrade",
|
||||
"6": "LookingToPlay",
|
||||
"7": "Invisible",
|
||||
};
|
||||
34
resources/EPersonaStateFlag.js
Normal file
34
resources/EPersonaStateFlag.js
Normal file
@@ -0,0 +1,34 @@
|
||||
/* eslint-disable */
|
||||
|
||||
/**
|
||||
* @enum EPersonaStateFlag
|
||||
*/
|
||||
module.exports = {
|
||||
"HasRichPresence": 1,
|
||||
"InJoinableGame": 2,
|
||||
"Golden": 4,
|
||||
"RemotePlayTogether": 8,
|
||||
"OnlineUsingWeb": 256, // removed "renamed to ClientTypeWeb"
|
||||
"ClientTypeWeb": 256,
|
||||
"OnlineUsingMobile": 512, // removed "renamed to ClientTypeMobile"
|
||||
"ClientTypeMobile": 512,
|
||||
"OnlineUsingBigPicture": 1024, // removed "renamed to ClientTypeTenfoot"
|
||||
"ClientTypeTenfoot": 1024,
|
||||
"OnlineUsingVR": 2048, // removed "renamed to ClientTypeVR"
|
||||
"ClientTypeVR": 2048,
|
||||
"LaunchTypeGamepad": 4096,
|
||||
"LaunchTypeCompatTool": 8192,
|
||||
|
||||
// Value-to-name mapping for convenience
|
||||
"1": "HasRichPresence",
|
||||
"2": "InJoinableGame",
|
||||
"4": "Golden",
|
||||
"8": "RemotePlayTogether",
|
||||
"256": "ClientTypeWeb",
|
||||
"512": "ClientTypeMobile",
|
||||
"1024": "ClientTypeTenfoot",
|
||||
"2048": "ClientTypeVR",
|
||||
"4096": "LaunchTypeGamepad",
|
||||
"8192": "LaunchTypeCompatTool",
|
||||
};
|
||||
|
||||
258
resources/EResult.js
Normal file
258
resources/EResult.js
Normal file
@@ -0,0 +1,258 @@
|
||||
/* eslint-disable */
|
||||
// Auto-generated by generate-enums script on Thu Jul 29 2021 04:43:52 GMT-0400 (Eastern Daylight Time)
|
||||
|
||||
/**
|
||||
* @enum EResult
|
||||
*/
|
||||
module.exports = {
|
||||
"Invalid": 0,
|
||||
"OK": 1,
|
||||
"Fail": 2,
|
||||
"NoConnection": 3,
|
||||
"InvalidPassword": 5,
|
||||
"LoggedInElsewhere": 6,
|
||||
"InvalidProtocolVer": 7,
|
||||
"InvalidParam": 8,
|
||||
"FileNotFound": 9,
|
||||
"Busy": 10,
|
||||
"InvalidState": 11,
|
||||
"InvalidName": 12,
|
||||
"InvalidEmail": 13,
|
||||
"DuplicateName": 14,
|
||||
"AccessDenied": 15,
|
||||
"Timeout": 16,
|
||||
"Banned": 17,
|
||||
"AccountNotFound": 18,
|
||||
"InvalidSteamID": 19,
|
||||
"ServiceUnavailable": 20,
|
||||
"NotLoggedOn": 21,
|
||||
"Pending": 22,
|
||||
"EncryptionFailure": 23,
|
||||
"InsufficientPrivilege": 24,
|
||||
"LimitExceeded": 25,
|
||||
"Revoked": 26,
|
||||
"Expired": 27,
|
||||
"AlreadyRedeemed": 28,
|
||||
"DuplicateRequest": 29,
|
||||
"AlreadyOwned": 30,
|
||||
"IPNotFound": 31,
|
||||
"PersistFailed": 32,
|
||||
"LockingFailed": 33,
|
||||
"LogonSessionReplaced": 34,
|
||||
"ConnectFailed": 35,
|
||||
"HandshakeFailed": 36,
|
||||
"IOFailure": 37,
|
||||
"RemoteDisconnect": 38,
|
||||
"ShoppingCartNotFound": 39,
|
||||
"Blocked": 40,
|
||||
"Ignored": 41,
|
||||
"NoMatch": 42,
|
||||
"AccountDisabled": 43,
|
||||
"ServiceReadOnly": 44,
|
||||
"AccountNotFeatured": 45,
|
||||
"AdministratorOK": 46,
|
||||
"ContentVersion": 47,
|
||||
"TryAnotherCM": 48,
|
||||
"PasswordRequiredToKickSession": 49,
|
||||
"AlreadyLoggedInElsewhere": 50,
|
||||
"Suspended": 51,
|
||||
"Cancelled": 52,
|
||||
"DataCorruption": 53,
|
||||
"DiskFull": 54,
|
||||
"RemoteCallFailed": 55,
|
||||
"PasswordNotSet": 56, // removed "renamed to PasswordUnset"
|
||||
"PasswordUnset": 56,
|
||||
"ExternalAccountUnlinked": 57,
|
||||
"PSNTicketInvalid": 58,
|
||||
"ExternalAccountAlreadyLinked": 59,
|
||||
"RemoteFileConflict": 60,
|
||||
"IllegalPassword": 61,
|
||||
"SameAsPreviousValue": 62,
|
||||
"AccountLogonDenied": 63,
|
||||
"CannotUseOldPassword": 64,
|
||||
"InvalidLoginAuthCode": 65,
|
||||
"AccountLogonDeniedNoMailSent": 66, // removed "renamed to AccountLogonDeniedNoMail"
|
||||
"AccountLogonDeniedNoMail": 66,
|
||||
"HardwareNotCapableOfIPT": 67,
|
||||
"IPTInitError": 68,
|
||||
"ParentalControlRestricted": 69,
|
||||
"FacebookQueryError": 70,
|
||||
"ExpiredLoginAuthCode": 71,
|
||||
"IPLoginRestrictionFailed": 72,
|
||||
"AccountLocked": 73, // removed "renamed to AccountLockedDown"
|
||||
"AccountLockedDown": 73,
|
||||
"AccountLogonDeniedVerifiedEmailRequired": 74,
|
||||
"NoMatchingURL": 75,
|
||||
"BadResponse": 76,
|
||||
"RequirePasswordReEntry": 77,
|
||||
"ValueOutOfRange": 78,
|
||||
"UnexpectedError": 79,
|
||||
"Disabled": 80,
|
||||
"InvalidCEGSubmission": 81,
|
||||
"RestrictedDevice": 82,
|
||||
"RegionLocked": 83,
|
||||
"RateLimitExceeded": 84,
|
||||
"AccountLogonDeniedNeedTwoFactorCode": 85, // removed "renamed to AccountLoginDeniedNeedTwoFactor"
|
||||
"AccountLoginDeniedNeedTwoFactor": 85,
|
||||
"ItemOrEntryHasBeenDeleted": 86, // removed "renamed to ItemDeleted"
|
||||
"ItemDeleted": 86,
|
||||
"AccountLoginDeniedThrottle": 87,
|
||||
"TwoFactorCodeMismatch": 88,
|
||||
"TwoFactorActivationCodeMismatch": 89,
|
||||
"AccountAssociatedToMultiplePlayers": 90, // removed "renamed to AccountAssociatedToMultiplePartners"
|
||||
"AccountAssociatedToMultiplePartners": 90,
|
||||
"NotModified": 91,
|
||||
"NoMobileDeviceAvailable": 92, // removed "renamed to NoMobileDevice"
|
||||
"NoMobileDevice": 92,
|
||||
"TimeIsOutOfSync": 93, // removed "renamed to TimeNotSynced"
|
||||
"TimeNotSynced": 93,
|
||||
"SMSCodeFailed": 94,
|
||||
"TooManyAccountsAccessThisResource": 95, // removed "renamed to AccountLimitExceeded"
|
||||
"AccountLimitExceeded": 95,
|
||||
"AccountActivityLimitExceeded": 96,
|
||||
"PhoneActivityLimitExceeded": 97,
|
||||
"RefundToWallet": 98,
|
||||
"EmailSendFailure": 99,
|
||||
"NotSettled": 100,
|
||||
"NeedCaptcha": 101,
|
||||
"GSLTDenied": 102,
|
||||
"GSOwnerDenied": 103,
|
||||
"InvalidItemType": 104,
|
||||
"IPBanned": 105,
|
||||
"GSLTExpired": 106,
|
||||
"InsufficientFunds": 107,
|
||||
"TooManyPending": 108,
|
||||
"NoSiteLicensesFound": 109,
|
||||
"WGNetworkSendExceeded": 110,
|
||||
"AccountNotFriends": 111,
|
||||
"LimitedUserAccount": 112,
|
||||
"CantRemoveItem": 113,
|
||||
"AccountHasBeenDeleted": 114,
|
||||
"AccountHasAnExistingUserCancelledLicense": 115,
|
||||
"DeniedDueToCommunityCooldown": 116,
|
||||
"NoLauncherSpecified": 117,
|
||||
"MustAgreeToSSA": 118,
|
||||
"ClientNoLongerSupported": 119, // obsolete
|
||||
"LauncherMigrated": 119,
|
||||
|
||||
// Value-to-name mapping for convenience
|
||||
"0": "Invalid",
|
||||
"1": "OK",
|
||||
"2": "Fail",
|
||||
"3": "NoConnection",
|
||||
"5": "InvalidPassword",
|
||||
"6": "LoggedInElsewhere",
|
||||
"7": "InvalidProtocolVer",
|
||||
"8": "InvalidParam",
|
||||
"9": "FileNotFound",
|
||||
"10": "Busy",
|
||||
"11": "InvalidState",
|
||||
"12": "InvalidName",
|
||||
"13": "InvalidEmail",
|
||||
"14": "DuplicateName",
|
||||
"15": "AccessDenied",
|
||||
"16": "Timeout",
|
||||
"17": "Banned",
|
||||
"18": "AccountNotFound",
|
||||
"19": "InvalidSteamID",
|
||||
"20": "ServiceUnavailable",
|
||||
"21": "NotLoggedOn",
|
||||
"22": "Pending",
|
||||
"23": "EncryptionFailure",
|
||||
"24": "InsufficientPrivilege",
|
||||
"25": "LimitExceeded",
|
||||
"26": "Revoked",
|
||||
"27": "Expired",
|
||||
"28": "AlreadyRedeemed",
|
||||
"29": "DuplicateRequest",
|
||||
"30": "AlreadyOwned",
|
||||
"31": "IPNotFound",
|
||||
"32": "PersistFailed",
|
||||
"33": "LockingFailed",
|
||||
"34": "LogonSessionReplaced",
|
||||
"35": "ConnectFailed",
|
||||
"36": "HandshakeFailed",
|
||||
"37": "IOFailure",
|
||||
"38": "RemoteDisconnect",
|
||||
"39": "ShoppingCartNotFound",
|
||||
"40": "Blocked",
|
||||
"41": "Ignored",
|
||||
"42": "NoMatch",
|
||||
"43": "AccountDisabled",
|
||||
"44": "ServiceReadOnly",
|
||||
"45": "AccountNotFeatured",
|
||||
"46": "AdministratorOK",
|
||||
"47": "ContentVersion",
|
||||
"48": "TryAnotherCM",
|
||||
"49": "PasswordRequiredToKickSession",
|
||||
"50": "AlreadyLoggedInElsewhere",
|
||||
"51": "Suspended",
|
||||
"52": "Cancelled",
|
||||
"53": "DataCorruption",
|
||||
"54": "DiskFull",
|
||||
"55": "RemoteCallFailed",
|
||||
"56": "PasswordUnset",
|
||||
"57": "ExternalAccountUnlinked",
|
||||
"58": "PSNTicketInvalid",
|
||||
"59": "ExternalAccountAlreadyLinked",
|
||||
"60": "RemoteFileConflict",
|
||||
"61": "IllegalPassword",
|
||||
"62": "SameAsPreviousValue",
|
||||
"63": "AccountLogonDenied",
|
||||
"64": "CannotUseOldPassword",
|
||||
"65": "InvalidLoginAuthCode",
|
||||
"66": "AccountLogonDeniedNoMail",
|
||||
"67": "HardwareNotCapableOfIPT",
|
||||
"68": "IPTInitError",
|
||||
"69": "ParentalControlRestricted",
|
||||
"70": "FacebookQueryError",
|
||||
"71": "ExpiredLoginAuthCode",
|
||||
"72": "IPLoginRestrictionFailed",
|
||||
"73": "AccountLockedDown",
|
||||
"74": "AccountLogonDeniedVerifiedEmailRequired",
|
||||
"75": "NoMatchingURL",
|
||||
"76": "BadResponse",
|
||||
"77": "RequirePasswordReEntry",
|
||||
"78": "ValueOutOfRange",
|
||||
"79": "UnexpectedError",
|
||||
"80": "Disabled",
|
||||
"81": "InvalidCEGSubmission",
|
||||
"82": "RestrictedDevice",
|
||||
"83": "RegionLocked",
|
||||
"84": "RateLimitExceeded",
|
||||
"85": "AccountLoginDeniedNeedTwoFactor",
|
||||
"86": "ItemDeleted",
|
||||
"87": "AccountLoginDeniedThrottle",
|
||||
"88": "TwoFactorCodeMismatch",
|
||||
"89": "TwoFactorActivationCodeMismatch",
|
||||
"90": "AccountAssociatedToMultiplePartners",
|
||||
"91": "NotModified",
|
||||
"92": "NoMobileDevice",
|
||||
"93": "TimeNotSynced",
|
||||
"94": "SMSCodeFailed",
|
||||
"95": "AccountLimitExceeded",
|
||||
"96": "AccountActivityLimitExceeded",
|
||||
"97": "PhoneActivityLimitExceeded",
|
||||
"98": "RefundToWallet",
|
||||
"99": "EmailSendFailure",
|
||||
"100": "NotSettled",
|
||||
"101": "NeedCaptcha",
|
||||
"102": "GSLTDenied",
|
||||
"103": "GSOwnerDenied",
|
||||
"104": "InvalidItemType",
|
||||
"105": "IPBanned",
|
||||
"106": "GSLTExpired",
|
||||
"107": "InsufficientFunds",
|
||||
"108": "TooManyPending",
|
||||
"109": "NoSiteLicensesFound",
|
||||
"110": "WGNetworkSendExceeded",
|
||||
"111": "AccountNotFriends",
|
||||
"112": "LimitedUserAccount",
|
||||
"113": "CantRemoveItem",
|
||||
"114": "AccountHasBeenDeleted",
|
||||
"115": "AccountHasAnExistingUserCancelledLicense",
|
||||
"116": "DeniedDueToCommunityCooldown",
|
||||
"117": "NoLauncherSpecified",
|
||||
"118": "MustAgreeToSSA",
|
||||
"119": "LauncherMigrated",
|
||||
};
|
||||
13
resources/ESharedFileType.js
Normal file
13
resources/ESharedFileType.js
Normal file
@@ -0,0 +1,13 @@
|
||||
/**
|
||||
* @enum ESharedFileType
|
||||
*/
|
||||
module.exports = {
|
||||
"Screenshot": 0,
|
||||
"Artwork": 1,
|
||||
"Guide": 2,
|
||||
|
||||
// Value-to-name mapping for convenience
|
||||
"0": "Screenshot",
|
||||
"1": "Artwork",
|
||||
"2": "Guide"
|
||||
};
|
||||
Reference in New Issue
Block a user