Compare commits

..

290 Commits
v3.33.1 ... v4

Author SHA1 Message Date
Alex Corn
4ec372b44b Added packageName and packageVersion properties 2023-07-06 21:52:31 -04:00
Alex Corn
25dfa2cdd2 Rename uri to url everywhere 2023-06-27 04:31:25 -04:00
Alex Corn
c006526f69 Pass httpProxy down to steam-session 2023-06-27 04:28:28 -04:00
Alex Corn
8e1f214612 Log into steam using steam-session 2023-06-27 04:27:39 -04:00
Alex Corn
38d3fb39bf Updated webapi.js methods to use new http interface 2023-06-27 04:11:04 -04:00
Alex Corn
e65d50b5f7 Updated twofactor.js methods to use new http interface 2023-06-27 04:06:40 -04:00
Alex Corn
3a65768b3f Updated sharedfiles.js methods to use new http interface 2023-06-27 03:58:13 -04:00
Alex Corn
96e3e7ebc0 Updated help.js methods to use new http interface 2023-06-27 03:50:52 -04:00
Alex Corn
e9bc778e2a Removed request dependency 2023-06-27 03:45:04 -04:00
Alex Corn
c1901d5f55 Updated confirmations.js methods to use new http interface 2023-06-27 03:38:56 -04:00
Alex Corn
028ee43bda Updated index.js methods to use new http interface 2023-06-27 02:56:05 -04:00
Alex Corn
0ba889ee7e Fixed merge conflict remnant 2023-06-27 02:55:47 -04:00
Alex Corn
ccf16b64e6 Updated httpRequest to use StdLib HttpClient 2023-06-27 02:07:42 -04:00
Alex Corn
078e39584e Remove got dependency 2023-06-27 01:22:36 -04:00
Alex Corn
2094f0545e Update xml2js dependency 2023-06-27 01:21:31 -04:00
Alex Corn
b48e8fa537 Promote steam-session to a full dependency 2023-06-27 01:21:01 -04:00
Alex Corn
2bfaecd88f Removed oAuthLogin method 2023-06-27 01:20:22 -04:00
Alex Corn
f776d9fd4a Removed getWebApiOauthToken 2023-06-27 01:20:06 -04:00
Alex Corn
86e87e88ed Merge branch 'master' into v4
# Conflicts:
#	components/confirmations.js
#	components/inventoryhistory.js
#	components/twofactor.js
#	components/users.js
#	components/webapi.js
#	index.js
#	package.json
2023-06-27 01:19:00 -04:00
Alex Corn
f69519f547 Fixed getUserInventoryContexts not properly returning errors for private inventory/profile
Closes #305
2023-06-27 00:53:55 -04:00
DoctorMcKay
29e18806cc Merge pull request #301 from fjexe/patch-1
Update users.js
2023-06-27 00:43:07 -04:00
DoctorMcKay
c855b505c5 Merge pull request #307 from HerrEurobeat/reenable-primaryGroup-support
Re-enable primaryGroup profile setting
2023-06-27 00:42:46 -04:00
Alex Corn
6cc96b51fa 3.46.1 2023-06-27 00:17:17 -04:00
DoctorMcKay
edf2891800 Merge pull request #314 from HerrEurobeat/fix-sharedfile-owner
Fix resolving vanity returning error
2023-06-27 00:16:17 -04:00
3urobeat
0fc1120fd3 Fix resolving vanity #313 2023-06-26 16:02:15 +02:00
DoctorMcKay
a0aee350cc Update comment 2023-06-26 01:37:08 -04:00
Alex Corn
2abc547f49 3.46.0 2023-06-24 03:00:14 -04:00
Alex Corn
b2bffddfbe Fixed wrong owner property 2023-06-24 02:53:15 -04:00
Alex Corn
ab9baeafc6 Return postDate as a Date object 2023-06-24 02:48:26 -04:00
Alex Corn
788f34ba7a Export ESharedFileType 2023-06-24 02:44:12 -04:00
Alex Corn
bde2cafaf4 We never want to return a sharedfile without an owner 2023-06-24 02:43:09 -04:00
Alex Corn
36e8c79d87 Some minor updates 2023-06-24 02:40:48 -04:00
DoctorMcKay
4ca0fc83aa Merge pull request #306 from HerrEurobeat/feature/steam-sharedfiles
Add full sharedfiles support
2023-06-24 02:30:12 -04:00
Alex Corn
2dd5bb490e Added accept_all_confirmations.js example 2023-06-23 18:32:50 -04:00
Alex Corn
d0da148d6b Added comment explaining usage of setMobileAppAccessToken 2023-06-23 01:04:48 -04:00
Alex Corn
7605dc8a41 Added examples readme 2023-06-23 01:02:20 -04:00
Alex Corn
5f46aee9fc Updated enable_twofactor and disable_twofactor examples to use steam-session 2023-06-23 01:00:09 -04:00
Alex Corn
02cc5daafe 3.45.3 2023-06-22 00:24:21 -04:00
Alex Corn
e76d34891a Return a descriptive error message to getConfirmations, if available 2023-06-22 00:13:28 -04:00
Alex Corn
20ad260d68 Remove defunct david-dm badge 2023-06-20 22:04:37 -04:00
Alex Corn
76f2c55a73 3.45.2 2023-06-20 21:54:50 -04:00
Alex Corn
77c2811e7e Use new confirmation tags in acceptConfirmationForObject 2023-06-20 21:54:44 -04:00
Alex Corn
37bac4d240 Make CConfirmation.timestamp a Date object 2023-06-20 21:48:30 -04:00
Alex Corn
d23d7e22c0 3.45.1 2023-06-20 21:42:54 -04:00
Alex Corn
6e19ffd93b Use new endpoints for trade confirmations 2023-06-20 21:42:28 -04:00
3urobeat
e5fbddc00c Merge branch 'master' into feature/steam-sharedfiles 2023-06-15 09:17:35 +00:00
Alex Corn
3531560083 3.45.0 2023-06-14 20:52:40 -04:00
Alex Corn
6fa6a073a8 Updated 2FA methods to use mobile app access token 2023-06-14 20:52:12 -04:00
Alex Corn
fd872490c8 3.44.4 2023-06-14 20:10:14 -04:00
Alex Corn
be528d1a19 Change default value for disableMobile to true 2023-06-14 20:10:00 -04:00
3urobeat
440b2f9ba9 Add JsDoc 2023-05-29 19:29:38 +02:00
3urobeat
e23fa02c91 Add support for reading numRatings of guides 2023-05-28 14:03:01 +02:00
3urobeat
33bc8d83c9 Add support for determining up/downvote status 2023-05-28 13:45:57 +02:00
3urobeat
f96d25ad52 Rename sid to sharedFileId 2023-05-15 22:45:44 +02:00
3urobeat
f7d7cf0660 Remove support for up- & downvoting sharedfiles 2023-05-15 22:42:38 +02:00
3urobeat
27d2daac61 Remove steamid-resolver dep and use internal helper 2023-05-15 22:37:52 +02:00
3urobeat
4723bd93a6 Improve resolveVanityURL() and move to helpers 2023-05-15 22:37:52 +02:00
3urobeat
897ad16154 Formatting 2023-05-14 18:52:52 +02:00
3urobeat
c25cb31e34 Re-enable primaryGroup profile setting 2023-05-14 17:06:56 +02:00
3urobeat
81d8dc2a5c Use decodeSteamTime() helper instead of doing it manually 2023-05-14 16:29:43 +02:00
3urobeat
8538589fef Fix owner remaining null and wrong param 2023-05-14 16:29:42 +02:00
3urobeat
c0be17cd61 Load CSteamSharedfile class 2023-05-14 16:29:42 +02:00
3urobeat
350f6288e4 Add sharedfile object methods 2023-05-14 16:29:42 +02:00
3urobeat
ba2782066d Misc 2023-05-14 16:29:41 +02:00
3urobeat
d6b0fbd5d0 Update non-object methods to take appid param 2023-05-14 16:29:40 +02:00
3urobeat
9ec5dcd7b0 Add sharedfile class with fully working scraper 2023-05-14 16:29:40 +02:00
3urobeat
c8642865a1 Add sharedfile type enum 2023-05-14 16:29:40 +02:00
3urobeat
c92801d25e Load sharedfiles component 2023-05-13 16:09:53 +02:00
3urobeat
b55516f67f Oops, wrong object name 2023-05-13 15:35:46 +02:00
3urobeat
f09adc84a4 Add (disabled) sharedfile favorite support 2023-05-13 15:35:46 +02:00
3urobeat
d807f10653 Add sharedfile subscribing support 2023-05-13 15:35:45 +02:00
3urobeat
18011b3fac Add sharedfile voting support 2023-05-13 15:35:45 +02:00
3urobeat
6f97370710 Add sharedfile comment support 2023-05-13 15:35:45 +02:00
Alex Corn
711ab8a3ae Update stdlib version 2023-04-26 14:22:09 -04:00
Alex Corn
9218ec6dd9 Use stdlib 2.1.0 2023-04-24 23:50:24 -04:00
Alex Corn
d28c2cdde9 Require node 14 2023-04-24 18:01:19 -04:00
Alex Corn
b98997a595 Merge branch 'master' into v4
# Conflicts:
#	components/http.js
#	components/users.js
#	package.json
2023-04-24 17:57:25 -04:00
fjexe
2a1d244114 Update users.js 2022-11-04 18:48:35 +03:00
Alex Corn
3bcaf40850 3.44.3 2022-10-28 04:31:42 -04:00
DoctorMcKay
0ccac69bea Merge pull request #300 from misterdudu/patch-1
Fix: New rate limit rule from steam
2022-10-28 04:30:41 -04:00
Eduardo
48d63857e2 Fix: New rate limit rule from steam
Steam added a new rule for inventories. Requesting with a "count" higher than 2000 will ban the proxy after four requests. All requests made with less than 2001 will have a rate limit of around 60/min per proxy.
2022-10-28 04:34:16 -03:00
Alex Corn
b11734cd9b 3.44.2 2022-08-24 21:47:03 -04:00
DoctorMcKay
5c37c38dda Merge pull request #291 from Heartz66/patch-2
Fix Not Logged In
2022-08-24 21:46:46 -04:00
Heartz66
260bfaa0ad Fix Not Logged In
Steam recently changed their login pages. This change should correctly identify the login state.
2022-08-24 23:59:51 +02:00
Alex Corn
1031a4dbd7 3.44.1 2022-05-13 22:49:06 -04:00
Alex Corn
e28fe5ca05 Merge pull request #286 from Heartz66/patch-1
Fix Not Logged In
2022-05-13 22:44:56 -04:00
Heartz66
90a9ee1aa9 Fix Not Logged In
Current version doesn't correctly identify login state. This change should correctly identify the state.
2022-05-13 17:10:52 +02:00
Alex Corn
7ebbd5806f Merge branch 'master' into v4
# Conflicts:
#	.idea/codeStyles/Project.xml
#	.idea/codeStyles/codeStyleConfig.xml
#	package.json
2022-02-02 03:43:42 -05:00
Alex Corn
9a069e80fb 3.44.0 2022-02-02 03:36:23 -05:00
Alex Corn
640c47b933 Updated comment 2022-02-02 03:35:44 -05:00
Alex Corn
f5d65a9ad4 Added getBoosterPackCatalog and createBoosterPack 2022-02-02 03:15:50 -05:00
Alex Corn
43c0504232 Require node 12 or later 2021-07-30 02:07:04 -04:00
Alex Corn
48f0637c18 Use steamid@2.0.0 2021-07-30 02:06:58 -04:00
Alex Corn
ad095605b6 Clean up comment parsing code a little 2021-07-30 00:39:46 -04:00
Alex Corn
d09f18f055 Compare against PrivacyState rather than using a magic number 2021-07-30 00:27:21 -04:00
Alex Corn
c7a8c676b7 Simplify isSteamID 2021-07-30 00:26:11 -04:00
Alex Corn
fd5ed31afa Ignore unused vars starting with underscore 2021-07-29 22:55:26 -04:00
Alex Corn
e2ef03cc32 Disallow spaces before anonymous/named function parens 2021-07-29 22:09:06 -04:00
Alex Corn
e49c5c175e Fix dot-notation violation 2021-07-29 22:04:20 -04:00
Alex Corn
565132d1bc Enforce dot-notation 2021-07-29 22:03:14 -04:00
Alex Corn
7c6e531dbd Added rationale for not using eqeqeq 2021-07-29 21:58:56 -04:00
Alex Corn
ac19e0867b Start adding callback promises 2021-07-29 05:55:12 -04:00
Alex Corn
e9d3221ee6 Start replacing request with got 2021-07-29 05:47:24 -04:00
Alex Corn
dfda22cee4 Rename ConfirmationType to EConfirmationType for consistency 2021-07-29 05:20:34 -04:00
Alex Corn
485529e807 Use npm run lint in workflow 2021-07-29 05:06:21 -04:00
Alex Corn
3def387d56 Updated @doctormckay/stdlib 2021-07-29 05:06:01 -04:00
Alex Corn
605fb6268f Skip eslint workflow for internal pull requests
Refs:
- https://github.community/t/duplicate-checks-on-push-and-pull-request-simultaneous-event/18012/4
- 612732d587
2021-07-29 05:03:07 -04:00
Alex Corn
89d058fc99 Update EResult.js 2021-07-29 04:54:27 -04:00
Alex Corn
9d47d28a1e Some more refactoring 2021-07-29 03:20:14 -04:00
Alex Corn
40ee35ac89 Fixed last eslint error 2021-07-29 03:15:47 -04:00
Alex Corn
e252efc455 Change all eslint rules to errors so they'll hit on CI 2021-07-29 03:15:38 -04:00
Alex Corn
a4de664759 Disable eslint on generated files 2021-07-29 03:14:55 -04:00
Alex Corn
59df4dee1d eslint requires node 10 2021-07-29 03:11:03 -04:00
Alex Corn
ca887c46ba Use npx for eslint 2021-07-29 03:10:06 -04:00
Alex Corn
8c0994cec3 Fixed eslint action 2021-07-29 03:09:20 -04:00
Alex Corn
9ec329569f Added eslint action 2021-07-29 03:05:54 -04:00
Alex Corn
600a41143b Refactor all files 2021-07-29 02:55:56 -04:00
Alex Corn
4c59309eb4 Refactor index.js 2021-07-29 02:41:04 -04:00
Alex Corn
2adc23f0ac Added eslint config 2021-07-29 02:30:41 -04:00
Alex Corn
5b40683873 Merge branch 'master' into v4
# Conflicts:
#	components/help.js
2021-07-23 00:54:50 -04:00
Alex Corn
7ae8a97a0f Slight refactor for help.js component 2021-07-22 23:58:47 -04:00
Alex Corn
63b0b82531 Cleaned up users.js 2021-07-22 04:42:48 -04:00
Alex Corn
2b678bea06 Some more cleanup 2021-07-22 04:22:29 -04:00
Alex Corn
8ffa0b50d4 Cleaned up help.js 2021-07-22 04:10:20 -04:00
Alex Corn
d093466d27 Removed getWebApiOauthToken 2021-07-22 04:06:59 -04:00
Alex Corn
c69a5451eb Cleaned up webapi.js 2021-07-22 04:02:38 -04:00
Alex Corn
9a4be453ec Cleaned up twofactor.js 2021-07-22 03:59:55 -04:00
Alex Corn
c550b380fc Cleaned up profile.js 2021-07-22 03:54:38 -04:00
Alex Corn
247d804c77 Cleaned up market.js 2021-07-22 03:44:40 -04:00
Alex Corn
dfd0ccbbc7 Merge branch 'master' into v4
# Conflicts:
#	components/confirmations.js
#	components/groups.js
#	components/market.js
#	package.json
2021-07-22 03:37:06 -04:00
Alex Corn
ec1284e961 Make callback optional in removePackage/restorePackage 2021-07-22 03:30:42 -04:00
Alex Corn
dc4bb1b554 3.43.1 2021-07-22 03:25:57 -04:00
Alex Corn
cd90d7a563 Trim comments before returning them 2021-07-22 03:25:47 -04:00
Alex Corn
851c14bd93 3.43.0 2021-07-22 03:09:56 -04:00
Alex Corn
24a42e9658 Export EFriendRelationship with the E prefix 2021-07-22 03:05:23 -04:00
Alex Corn
8ec671d952 Reformat 2021-07-22 03:04:38 -04:00
Alex Corn
2821a5900c Merge pull request #267 from Revadike/feature-get-friends-list
Added getFriendsList
2021-07-22 03:04:16 -04:00
Alex Corn
2da6725283 Merge branch 'master' into feature-get-friends-list 2021-07-22 03:04:06 -04:00
Alex Corn
23a7940064 Merge branch 'master' into feature-get-friends-list 2021-07-22 03:03:24 -04:00
Alex Corn
165c963b1a Create new help component and move restore/removePackage to it 2021-07-22 03:01:57 -04:00
Alex Corn
429a76edcc Reformat 2021-07-22 02:56:03 -04:00
Alex Corn
3a224ff45f Merge pull request #262 from Revadike/feature-package-control
Added restorePackage & removePackage
2021-07-22 02:55:01 -04:00
Alex Corn
dfad525e4e Merge pull request #220 from Revadike/feature/profile-comments
Added deleteUserComment and getUserComments
2021-07-22 02:34:32 -04:00
Revadike
81877f4bb7 Use object.assign instead 2021-07-22 07:48:46 +02:00
Alex Corn
f6a3186865 Merge pull request #268 from Revadike/enums-update
Updated Enums
2021-06-05 02:41:02 -04:00
Revadike
e0687f8828 Updated Enums 2021-06-04 04:24:11 +02:00
Revadike
d54dd2c892 Added getFriendsList 2021-06-04 04:08:07 +02:00
Revadike
b4818d6d67 Bugfix 2021-06-04 03:26:24 +02:00
Revadike
d8eb07f046 re-use variable
Co-authored-by: Mr-VIT <vit@v1t.su>
2021-05-13 01:14:52 +02:00
Revadike
e6995e337e Added restorePackage & removePackage 2021-03-19 15:22:41 +01:00
Alex Corn
2bbdbf36ad 3.42.0 2020-11-05 02:55:53 -05:00
Alex Corn
e6311c0fcb Removed attempt to get oauth token from webchat 2020-11-05 02:42:31 -05:00
Alex Corn
2fc8d1f4aa Refactored gem sack code 2020-11-05 02:19:31 -05:00
Jack Nolddor
b00b871a5b Added gems/sack transformations (#260)
Added Sack of Gems <---> Gems transformations
2020-11-05 01:31:35 -05:00
Alex Corn
15079621d2 3.41.8 2020-10-03 04:54:58 -04:00
Alex Corn
d91c600eae Merge pull request #258 from theanht1/master
Fix parentalUnlock
2020-10-02 02:17:35 -04:00
Anh Mai
0f98403c16 feat: Add sessionid to form of parentalUnlock 2020-09-30 17:43:00 +07:00
Alex Corn
d9b6b55efc 3.41.7 2020-09-23 18:11:33 -04:00
Alex Corn
0fc62d2966 Merge pull request #257 from timi-owo/master
Fixed 'language' was missing from the 'CSteamUser.getInventoryContents'
2020-09-23 17:46:59 -04:00
timi-owo
5daf5f712a Fixed param 'language' was missing when calling 'CSteamUser.getInventoryContents' method 2020-09-24 01:44:32 +08:00
Alex Corn
737aa9dc41 Added comment 2020-07-01 00:52:15 -04:00
Alex Corn
07fb26047b 3.41.6 2020-06-30 22:36:48 -04:00
Alex Corn
2f9b24c447 Only attempt to delete this.currency if it actually exists 2020-06-30 22:24:04 -04:00
Alex Corn
211c625da0 3.41.5 2020-06-26 03:45:17 -04:00
Alex Corn
2e61a6ed70 Fixed editProfile and profileSettings for Steam update 2020-06-26 03:44:58 -04:00
Alex Corn
f0a3cb27f8 Merge pull request #250 from itsjfx/open-booster-pack-patch
Add openBoosterPack
2020-05-28 22:30:35 -04:00
Alex Corn
6ed33a100e 3.41.4 2020-05-28 00:15:39 -04:00
Alex Corn
d41a52638b Prevent timeOffset delete timeout from holding app open 2020-05-28 00:15:20 -04:00
jfx
58516d4ed1 Add openBoosterPack 2020-05-23 16:24:20 +10:00
Alex Corn
3f99c718d4 Modernized disable_twofactor example script 2020-04-04 21:53:34 -04:00
Alex Corn
b92c5d852d Modernized enable_twofactor example script 2020-04-04 21:53:32 -04:00
Alex Corn
f5da011cde Create FUNDING.yml 2020-03-10 03:51:16 -04:00
Alex Corn
3633ab032c 3.41.3 2019-11-08 03:33:06 -05:00
Alex Corn
76df74d724 Added hidden parameter to CSteamGroup#postAnnouncement 2019-11-08 03:33:00 -05:00
Alex Corn
8b5cd59021 v3.41.2 2019-11-08 03:27:57 -05:00
Alex Corn
25adcb101b 3.41.1 2019-11-08 03:24:42 -05:00
Alex Corn
d7ecad670d Merge pull request #237 from Heartz66/patch-1
Fix wrong date
2019-11-08 03:23:27 -05:00
Heartz66
8225ea5127 Fix wrong date
https://github.com/DoctorMcKay/node-steamcommunity/issues/236
2019-11-06 19:43:45 +01:00
Alex Corn
59bf4f1e92 Merge pull request #233 from Revadike/patch-1
Optional hidden parameter to postGroupAnnouncement
2019-10-15 01:13:03 +02:00
Revadike
a3468bf99b Optional hidden parameter to postGroupAnnouncement 2019-10-14 04:28:53 +02:00
Alex Corn
e299623452 Use different filenames for different images, similar to how Steam does it 2019-09-25 05:33:14 -04:00
Alex Corn
29b31009ce Merge remote-tracking branch 'origin/master' 2019-09-25 05:30:14 -04:00
Alex Corn
fa253920e3 Code cleanup for http.js 2019-09-24 04:43:51 -04:00
Alex Corn
30ebf333b4 Code cleanup for helpers.js 2019-09-24 04:41:11 -04:00
Alex Corn
d7cfc396a7 Code cleanup for groups.js 2019-09-24 04:38:27 -04:00
Alex Corn
ac8e4a62de Code cleanup for confirmations.js 2019-09-24 04:28:05 -04:00
Alex Corn
8e9e3723f2 Code cleanup for chat.js 2019-09-24 04:23:40 -04:00
Alex Corn
6b0f958cfd Merge branch 'master' into v4
# Conflicts:
#	index.js
#	package.json
2019-09-24 04:14:09 -04:00
Alex Corn
d4d0d7da26 3.41.0 2019-09-24 04:07:34 -04:00
Alex Corn
3c8488d441 Added method sendImageToUser 2019-09-24 04:06:37 -04:00
Alex Corn
3ea0995415 3.40.2 2019-09-21 04:27:55 -04:00
Alex Corn
21c4ba3e27 Restore cache_expiration from game-specific data for backwards compatibility
(Fixes #229)
2019-09-21 04:27:39 -04:00
Alex Corn
4d4df7c2a5 3.40.1 2019-09-07 05:06:06 -04:00
Alex Corn
aa0d4d32b7 Removed stats-reporter 2019-09-07 05:05:46 -04:00
Alex Corn
444a6d238d Merge pull request #225 from Revadike/patch-1
Added userID guard
2019-06-17 03:18:06 -04:00
Revadike
98373708e5 Added userID guard
Because I got this:
```
 2019-06-12 | 01:22:30.151 | ERROR | crashBot                       | Fatal error: TypeError: Cannot read property 'getSteamID64' of null
    at get (node_modules\steamcommunity\components\users.js:416:60)
    at SteamCommunity.getUserInventoryContents (node_modules\steamcommunity\components\users.js:412:2)
    at unpackSteamBoosterPacks (index.js:3588:21)
    at cleanupSteamInventory (index.js:3583:5)
    at SteamUser.refreshSteamWebSession (index.js:857:38)
    at Object.onceWrapper (events.js:273:13)
    at SteamUser.emit (events.js:182:13)
    at SteamUser.gotSteamWebSession (index.js:1754:16)
    at SteamUser.emit (events.js:182:13)
    at _apiRequest (node_modules\steam-user\components\web.js:61:9)
    at IncomingMessage.<anonymous> (node_modules\steam-user\components\webapi.js:82:4)
    at IncomingMessage.emit (events.js:187:15)
    at endReadableNT (_stream_readable.js:1094:12)
    at process._tickCallback (internal/process/next_tick.js:63:19)
```
2019-06-17 02:43:07 +02:00
Alex Corn
0df2662053 3.40.0 2019-04-19 01:28:43 -04:00
Alex Corn
a46ed17415 Added clearPersonaNameHistory 2019-04-19 01:20:01 -04:00
Revadike
9ed377e843 Oopsy 2019-04-05 00:13:02 +02:00
Revadike
868ce9cdb4 Added missing functions 2019-04-05 00:10:00 +02:00
Revadike
8edc5020e1 Added getUserComments(userID, options, callback)
https://github.com/DoctorMcKay/node-steamcommunity/issues/208
2019-04-05 00:06:48 +02:00
Revadike
f2be027a63 Updated postUserComment and added deleteUserComment
https://github.com/DoctorMcKay/node-steamcommunity/issues/219
2019-04-04 23:30:28 +02:00
Alex Corn
8a7d6f5018 Merge branch 'master' into v4 2019-02-14 00:31:20 -05:00
Alex Corn
fbfff1398b Merge remote-tracking branch 'origin/master'
# Conflicts:
#	.idea/encodings.xml
2019-02-14 00:29:44 -05:00
Alex Corn
0cb0d22433 Cleaned up imports 2019-02-14 00:29:21 -05:00
Alex Corn
103e941501 Updated dependencies 2019-02-14 00:25:07 -05:00
Alex Corn
f99d86aa73 Removed async dependency 2019-02-14 00:23:17 -05:00
Alex Corn
5a855316f6 Removed inventoryhistory.js 2019-02-14 00:22:51 -05:00
Alex Corn
729f078a25 Removed deprecated confirmation checker 2019-02-14 00:22:06 -05:00
Alex Corn
80dbf9d688 Start v4 2019-02-14 00:19:20 -05:00
Alex Corn
224b7b85e0 3.39.0 2019-02-13 17:59:41 -05:00
Alex Corn
22d34b1faf Added Steam ID to output of getClientLogonToken 2019-02-13 17:59:33 -05:00
Alex Corn
cfd932dc72 Changed output format of getClientLogonToken 2019-02-13 17:42:28 -05:00
Alex Corn
4710afa28b Added getClientLogonToken 2019-02-13 17:40:45 -05:00
Alex Corn
1180eb9d3f Deprecated webchat methods 2019-02-13 17:35:39 -05:00
Alex Corn
3ca85bedc2 Merge branch 'master' of ssh://github.com/DoctorMcKay/node-steamcommunity 2019-02-13 17:28:57 -05:00
Alex Corn
e05efabcb9 Merge pull request #211 from Hongarc/master
Add comment group
2019-02-12 21:35:58 -05:00
Alex Corn
6cd06c3adf Updated module type 2019-02-06 19:54:04 -05:00
Alex Corn
d35e3c107c Make sure CConfirmation.{id, creator} are always strings 2019-02-06 19:36:01 -05:00
Alex Corn
4e90811008 Merge pull request #213 from Royalgamer06/patch-2
Package 'fs' never used
2018-11-19 14:54:33 -05:00
Royalgamer06
018d3de5a2 Package 'fs' never used 2018-11-19 20:48:57 +01:00
Alex Corn
3949abe09f 3.38.0 2018-11-01 01:20:31 -04:00
Alex Corn
62394b9ffe Added ability to change friends list privacy 2018-11-01 01:20:19 -04:00
Alex Corn
ebac6c97ad Added IDE config to repo 2018-10-31 03:47:55 -04:00
Hongarc
a0ad33d5eb Add comment group 2018-10-20 13:29:52 +07:00
Alex Corn
b7a3e51bfc Merge pull request #205 from waylaidwanderer/master
Add the ability to set custom mobile user-agent
2018-07-11 02:31:04 -04:00
Joel
bf13224cbf Add the ability to set custom mobile user-agent 2018-07-10 23:30:12 -07:00
Alex Corn
117374a4c5 Updated Chrome user-agent 2018-07-11 02:22:39 -04:00
Alex Corn
aa84505132 3.37.1 2018-07-11 02:12:45 -04:00
Alex Corn
bb97565f2a Fixed steamID not being properly set 2018-07-11 02:12:37 -04:00
Alex Corn
7462e4b767 3.37.0 2018-07-11 00:00:16 -04:00
Alex Corn
adbf1d47a7 Added postProfileStatus and deleteProfileStatus (closes #200) 2018-07-10 23:59:57 -04:00
Alex Corn
8c4fde720a Updated EResult 2018-07-10 23:53:05 -04:00
Alex Corn
458d7b6db1 Renamed getProfileBackground to getUserProfileBackground and added it to CSteamUser 2018-07-10 23:28:26 -04:00
Alex Corn
411d74036f Updated getProfileBackground output a bit 2018-07-10 23:24:41 -04:00
Mert
1b3117b8dc Add getProfileBackground method
Adds ability to get URL of Steamcommunity profile background.
2018-07-10 23:18:03 -04:00
Alex Corn
6eec88d196 3.36.2 2018-05-18 17:32:48 -04:00
Alex Corn
3921ce17d0 Follow redirects when posting to own profile (fixes #178) 2018-05-18 17:32:33 -04:00
Alex Corn
4441d6f6e0 Bumped required request version 2018-05-18 17:29:14 -04:00
Alex Corn
870de6817e 3.36.1 2018-04-15 10:43:14 -04:00
Alex Corn
d5dbb230df Removed extra import 2018-04-15 10:43:05 -04:00
Alex Corn
5e9c06bdc4 3.36.0 2018-04-11 00:27:57 -04:00
Alex Corn
8d163d0d85 Added privacy controls for gameDetails and playtime 2018-04-11 00:27:10 -04:00
Alex Corn
051d376508 3.35.2 2018-04-10 01:08:14 -04:00
Alex Corn
913dff68ef Fixed crashes if somehow market items are really broken (fixes #197) 2018-04-10 01:06:52 -04:00
Alex Corn
754847ee88 Make CMarketItem not broken for non-CSGO apps (fixes #196) 2018-04-10 01:01:03 -04:00
Alex Corn
645e03b89c 3.35.1 2018-04-01 23:36:04 -04:00
Alex Corn
aa302bc09e Fixed various profile url requests 2018-04-01 23:35:56 -04:00
Alex Corn
1c0f8d4bca 3.35.0 2018-04-01 03:39:00 -04:00
Alex Corn
a7bd694f4b Added inventory gift redemption 2018-04-01 03:38:45 -04:00
Alex Corn
9e380e71f5 jsdoc 2018-04-01 03:31:07 -04:00
Alex Corn
e97e63fe6d Added ability to get gem value for an item and to turn items into gems 2018-04-01 03:22:00 -04:00
Alex Corn
f6c7c74ab6 Fixed Node.js process being kept running unnecessarily 2018-04-01 03:15:13 -04:00
Alex Corn
8846082f60 Fixed getMarketApps not returning all apps 2018-04-01 02:51:46 -04:00
Alexander Corn
eb609b5c42 Added some more explicit error handling if we try to do things while logged out 2018-02-27 01:40:59 -05:00
Alex Corn
238a36de0f v3.34.1 2018-01-22 02:02:56 -05:00
Alex Corn
c38111b61a Merge pull request #189 from Aareksio/patch-1
Fix infinite loop in getWebApiKey
2018-01-22 01:48:17 -05:00
Arkadiusz Sygulski
0eb27a15d2 Fix infinite loop in getWebApiKey
Fix infinite loop in getWebApiKey when trying to get web api key on account without email address confirmed
2018-01-22 03:48:45 +01:00
Alexander Corn
0ab3a94493 Use writeFileSync instead of writeFile without callback 2018-01-16 14:40:07 -05:00
Alexander Corn
7c2ec65b8c Add .editorconfig 2017-12-30 02:25:07 -05:00
Alexander Corn
8e761ffe6c 3.34.0 2017-12-28 00:34:23 -05:00
Alexander Corn
06f04887fd Updated CONTRIBUTING.md 2017-12-28 00:34:15 -05:00
Alexander Corn
e5f986a8bf Added author info 2017-12-28 00:33:01 -05:00
Alexander Corn
5da6e40ebb Fixed some code style issues 2017-12-28 00:32:06 -05:00
Alex Corn
0b8dd57a40 Merge pull request #186 from hell00world/master
Added changeTradeURL method
2017-12-28 00:21:00 -05:00
hell00world
6f8456882a Added changeTradeURL method 2017-12-28 00:19:57 +01:00
Alex Corn
0b57b47ecb Minor tweaks 2017-12-26 16:39:23 -05:00
Alex Corn
b5f50a6614 Style update 2017-12-26 16:34:19 -05:00
Alex Corn
4257140c15 Merge remote-tracking branch 'origin/master' 2017-12-26 16:34:02 -05:00
Alex Corn
724badd175 Merge pull request #163 from potato20/disable-mobile-flag
Flag to disable mobile login
2017-12-26 16:33:56 -05:00
Alex Corn
a9455b0ff6 Style updates, use HTTPS, coerce steamids to SteamId objects 2017-12-26 16:32:21 -05:00
Alex Corn
b3dfdd47d8 Fixed missing brace 2017-12-26 16:32:04 -05:00
Alex Corn
1efac6bc70 Merge pull request #159 from Jarzon/groupcomments
Steam group comments
2017-12-26 16:28:16 -05:00
Alex Corn
eacb85c711 3.33.2 2017-11-30 08:59:58 -05:00
Alex Corn
8eab34bf18 Fixed errors not being handled in getTradeURL 2017-11-30 08:59:49 -05:00
Jason Vaillancourt
dc2e5d09a7 Merge branch 'master' into groupcomments 2017-08-20 20:13:41 -04:00
Jarzon
3bba7025d4 Changes for code consistency 2017-03-15 13:08:22 -04:00
Jarzon
e2f94cb1e7 Convert the number to a string 2017-03-15 12:55:48 -04:00
Jarzon
91285ba1c7 Merge branch 'master' into groupcomments 2017-03-04 09:04:24 -05:00
Potato 2.0
c6ab392c37 Add disableMobile flag to login() so user has option to not pretend we're logging in from a phone 2017-02-18 14:13:15 -05:00
Jarzon
530bbcb9eb Pass a Error obj to the callback 2017-02-08 15:22:50 -05:00
Jarzon
f7ebe68bd8 Adding .deleteComment() 2017-02-08 15:08:34 -05:00
Jarzon
e2dae00af2 Simplifying the request's options 2017-02-07 18:34:12 -05:00
Jarzon
bbd80d4154 Fixing not getting the html in the comment 2017-02-02 07:53:18 -05:00
Jarzon
c180557f52 Using decodeSteamTime() to parse date
Adding to .decodeSteamTime() the ability to parse steam relative time
2017-02-01 13:30:38 -05:00
Jarzon
b3174b9f0b Revert using chrono to parse the date 2017-02-01 12:03:34 -05:00
Jarzon
12f1ddf6a0 Missing header Content-Type 2017-02-01 11:27:58 -05:00
Jarzon
db3aa4ace8 Catch the community id and the custom url of the author 2017-01-31 08:48:38 -05:00
Jarzon
f1b742dff4 Adding Chrono to parse relative date 2017-01-31 06:26:11 -05:00
Jarzon
444649ee8c Adding .getAllComments() 2017-01-31 06:16:38 -05:00
56 changed files with 4156 additions and 2562 deletions

13
.editorconfig Normal file
View 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
View 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
View File

@@ -0,0 +1,2 @@
github: DoctorMcKay
custom: 'https://www.paypal.com/cgi-bin/webscr?cmd=_donations&business=N36YVAT42CZ4G&item_name=node%2dsteamcommunity&currency_code=USD'

31
.github/workflows/eslint.yml vendored Normal file
View 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

53
.gitignore vendored
View File

@@ -1,3 +1,54 @@
node_modules/* node_modules/*
test.js test.js
.idea/ 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
View File

@@ -0,0 +1 @@
node-steamcommunity

13
.idea/codeStyleSettings.xml generated Normal file
View 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
View 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="&#10;" />
<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
View 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
View File

@@ -0,0 +1,3 @@
<component name="CopyrightManager">
<settings default="" />
</component>

6
.idea/encodings.xml generated Normal file
View 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>

View 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
View 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
View 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
View 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
View 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
View 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
View 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
View File

@@ -0,0 +1,4 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="ProjectTasksOptions" suppressed-tasks="Babel" />
</project>

View File

@@ -28,6 +28,7 @@ Submitting a pull request? Great! Thanks for contributing your time and code! Pl
- Tabs for indentation - Tabs for indentation
- camelCase for variables and functions - camelCase for variables and functions
- Opening braces on the same line as the if/for/while statement - Opening braces on the same line as the if/for/while statement
- No omitting unnecessary braces
- etc. - 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 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 don't increment the version number in `package.json`. I'll do that myself when I publish it to npm.

View File

@@ -1,6 +1,6 @@
The MIT License (MIT) The MIT License (MIT)
Copyright (c) 2015 Alexander Corn Copyright (c) 2015 Alex Corn
Permission is hereby granted, free of charge, to any person obtaining a copy Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal of this software and associated documentation files (the "Software"), to deal

View File

@@ -1,7 +1,6 @@
# Steam Community for Node.js # Steam Community for Node.js
[![npm version](https://img.shields.io/npm/v/steamcommunity.svg)](https://npmjs.com/package/steamcommunity) [![npm version](https://img.shields.io/npm/v/steamcommunity.svg)](https://npmjs.com/package/steamcommunity)
[![npm downloads](https://img.shields.io/npm/dm/steamcommunity.svg)](https://npmjs.com/package/steamcommunity) [![npm downloads](https://img.shields.io/npm/dm/steamcommunity.svg)](https://npmjs.com/package/steamcommunity)
[![dependencies](https://img.shields.io/david/DoctorMcKay/node-steamcommunity.svg)](https://david-dm.org/DoctorMcKay/node-steamcommunity)
[![license](https://img.shields.io/npm/l/steamcommunity.svg)](https://github.com/DoctorMcKay/node-steamcommunity/blob/master/LICENSE) [![license](https://img.shields.io/npm/l/steamcommunity.svg)](https://github.com/DoctorMcKay/node-steamcommunity/blob/master/LICENSE)
[![paypal](https://img.shields.io/badge/paypal-donate-yellow.svg)](https://www.paypal.com/cgi-bin/webscr?cmd=_donations&business=N36YVAT42CZ4G&item_name=node%2dsteamcommunity&currency_code=USD) [![paypal](https://img.shields.io/badge/paypal-donate-yellow.svg)](https://www.paypal.com/cgi-bin/webscr?cmd=_donations&business=N36YVAT42CZ4G&item_name=node%2dsteamcommunity&currency_code=USD)
@@ -9,8 +8,6 @@ This module provides an easy interface for the Steam Community website. This mod
It supports Steam Guard and CAPTCHAs. It supports Steam Guard and CAPTCHAs.
This reports anonymous usage statistics to the author. [See here](https://github.com/DoctorMcKay/node-stats-reporter) for more information.
**Have a question about the module or coding in general? *Do not create a GitHub issue.* GitHub issues are for feature **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/). requests and bug reports. Instead, post in the [dedicated forum](https://dev.doctormckay.com/forum/8-node-steamcommunity/).
Such issues may be ignored!** Such issues may be ignored!**

View File

@@ -1,35 +1,52 @@
var SteamCommunity = require('../index.js'); const StdLib = require('@doctormckay/stdlib');
const SteamCommunity = require('../index.js');
module.exports = CConfirmation; module.exports = CConfirmation;
function CConfirmation(community, data) { function CConfirmation(community, data) {
Object.defineProperty(this, "_community", {"value": community}); Object.defineProperty(this, '_community', {value: community});
this.id = data.id; this.id = data.id.toString();
this.type = data.type; this.type = data.type;
this.creator = data.creator; this.creator = data.creator.toString();
this.key = data.key; this.key = data.key;
this.title = data.title; this.title = data.title;
this.receiving = data.receiving; this.receiving = data.receiving;
this.sending = data.sending;
this.time = data.time; this.time = data.time;
this.timestamp = data.timestamp;
this.icon = data.icon; this.icon = data.icon;
this.offerID = this.type == SteamCommunity.ConfirmationType.Trade ? this.creator : null; 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) { CConfirmation.prototype.getOfferID = function(time, key, callback) {
if (this.type && this.creator) { return StdLib.Promises.callbackPromise(['offerID'], null, false, async (resolve, reject) => {
if (this.type != SteamCommunity.ConfirmationType.Trade) { if (this.type && this.creator) {
callback(new Error("Not a trade confirmation")); if (this.type != SteamCommunity.ConfirmationType.Trade) {
return; return reject(new Error('Not a trade confirmation'));
}
return resolve({offerID: this.creator});
} }
callback(null, this.creator); return await this._community.getConfirmationOfferID(this.id, time, key, callback);
return; });
}
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) { CConfirmation.prototype.respond = function(time, key, accept, callback) {
this._community.respondToConfirmation(this.id, this.key, time, key, accept, callback); return this._community.respondToConfirmation(this.id, this.key, time, key, accept, callback);
}; };

View File

@@ -1,14 +1,11 @@
module.exports = CEconItem; module.exports = CEconItem;
function CEconItem(item, description, contextID) { function CEconItem(item, description, contextID) {
var thing; for (let thing in item) {
for (thing in item) { this[thing] = item[thing];
if (item.hasOwnProperty(thing)) {
this[thing] = item[thing];
}
} }
var 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 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) { if (isCurrency) {
this.currencyid = this.id = (this.id || this.currencyid); this.currencyid = this.id = (this.id || this.currencyid);
@@ -27,10 +24,8 @@ function CEconItem(item, description, contextID) {
description = description[this.classid + '_' + this.instanceid]; description = description[this.classid + '_' + this.instanceid];
} }
for (thing in description) { for (let thing in description) {
if (description.hasOwnProperty(thing)) { this[thing] = description[thing];
this[thing] = description[thing];
}
} }
} }
@@ -49,40 +44,58 @@ function CEconItem(item, description, contextID) {
// Restore old property names of tags // Restore old property names of tags
if (this.tags) { if (this.tags) {
this.tags = this.tags.map(function(tag) { this.tags = this.tags.map((tag) => ({
return { internal_name: tag.internal_name,
"internal_name": tag.internal_name, name: tag.localized_tag_name || tag.name,
"name": tag.localized_tag_name || tag.name, category: tag.category,
"category": tag.category, color: tag.color || '',
"color": tag.color || "", category_name: tag.localized_category_name || tag.category_name
"category_name": tag.localized_category_name || tag.category_name }));
};
});
} }
// Restore market_fee_app, if applicable // Restore market_fee_app, if applicable
var match; let match;
if (this.appid == 753 && this.contextid == 6 && this.market_hash_name && (match = this.market_hash_name.match(/^(\d+)\-/))) { 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); this.market_fee_app = parseInt(match[1], 10);
} }
if (this.actions === "") { // 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 = []; this.actions = [];
} }
delete this.currency; // 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() { CEconItem.prototype.getImageURL = function() {
return "https://steamcommunity-a.akamaihd.net/economy/image/" + this.icon_url + "/"; return 'https://steamcommunity-a.akamaihd.net/economy/image/' + this.icon_url + '/';
}; };
CEconItem.prototype.getLargeImageURL = function() { CEconItem.prototype.getLargeImageURL = function() {
if(!this.icon_url_large) { if (!this.icon_url_large) {
return this.getImageURL(); return this.getImageURL();
} }
return "https://steamcommunity-a.akamaihd.net/economy/image/" + this.icon_url_large + "/"; return 'https://steamcommunity-a.akamaihd.net/economy/image/' + this.icon_url_large + '/';
}; };
CEconItem.prototype.getTag = function(category) { CEconItem.prototype.getTag = function(category) {
@@ -90,7 +103,7 @@ CEconItem.prototype.getTag = function(category) {
return null; return null;
} }
for (var i = 0; i < this.tags.length; i++) { for (let i = 0; i < this.tags.length; i++) {
if (this.tags[i].category == category) { if (this.tags[i].category == category) {
return this.tags[i]; return this.tags[i];
} }

View File

@@ -1,33 +1,35 @@
var SteamCommunity = require('../index.js'); const Cheerio = require('cheerio');
var Cheerio = require('cheerio');
const SteamCommunity = require('../index.js');
SteamCommunity.prototype.getMarketItem = function(appid, hashName, currency, callback) { SteamCommunity.prototype.getMarketItem = function(appid, hashName, currency, callback) {
if (typeof currency == "function") { if (typeof currency == 'function') {
callback = currency; callback = currency;
currency = 1; currency = 1;
} }
var self = this;
this.httpRequest("https://steamcommunity.com/market/listings/" + appid + "/" + encodeURIComponent(hashName), function(err, response, body) { this.httpRequest('https://steamcommunity.com/market/listings/' + appid + '/' + encodeURIComponent(hashName), (err, response, body) => {
if (err) { if (err) {
callback(err); callback(err);
return; return;
} }
var $ = Cheerio.load(body); let $ = Cheerio.load(body);
if($('.market_listing_table_message') && $('.market_listing_table_message').text().trim() == 'There are no listings for this item.') { let $listingTableMessage = $('.market_listing_table_message');
callback(new Error("There are no listings for this item.")); if ($listingTableMessage && $listingTableMessage.text().trim() == 'There are no listings for this item.') {
callback(new Error('There are no listings for this item.'));
return; return;
} }
var item = new CMarketItem(appid, hashName, self, body, $); let item = new CMarketItem(appid, hashName, this, body, $);
item.updatePrice(currency, function(err) { item.updatePrice(currency, (err) => {
if(err) { if (err) {
callback(err); callback(err);
} else { } else {
callback(null, item); callback(null, item);
} }
}); });
}, "steamcommunity"); }, 'steamcommunity');
}; };
function CMarketItem(appid, hashName, community, body, $) { function CMarketItem(appid, hashName, community, body, $) {
@@ -35,39 +37,37 @@ function CMarketItem(appid, hashName, community, body, $) {
this._hashName = hashName; this._hashName = hashName;
this._community = community; this._community = community;
this._$ = $; this._$ = $;
this._country = "US"; this._country = 'US';
var match = body.match(/var g_strCountryCode = "([^"]+)";/); let match = body.match(/var g_strCountryCode = "([^"]+)";/);
if(match) { if (match) {
this._country = match[1]; this._country = match[1];
} }
this._language = "english"; this._language = 'english';
match = body.match(/var g_strLanguage = "([^"]+)";/); match = body.match(/var g_strLanguage = "([^"]+)";/);
if(match) { if (match) {
this._language = match[1]; this._language = match[1];
} }
this.commodity = false; this.commodity = false;
var match = body.match(/Market_LoadOrderSpread\(\s*(\d+)\s*\);/); match = body.match(/Market_LoadOrderSpread\(\s*(\d+)\s*\);/);
if(match) { if (match) {
this.commodity = true; this.commodity = true;
this.commodityID = parseInt(match[1], 10); this.commodityID = parseInt(match[1], 10);
} }
this.medianSalePrices = null; this.medianSalePrices = null;
match = body.match(/var line1=([^;]+);/); match = body.match(/var line1=([^;]+);/);
if(match) { if (match) {
try { try {
this.medianSalePrices = JSON.parse(match[1]); this.medianSalePrices = JSON.parse(match[1]);
this.medianSalePrices = this.medianSalePrices.map(function(item) { this.medianSalePrices = this.medianSalePrices.map((item) => ({
return { hour: new Date(item[0]),
"hour": new Date(item[0]), price: item[1],
"price": item[1], quantity: parseInt(item[2], 10)
"quantity": parseInt(item[2], 10) }));
}; } catch (e) {
});
} catch(e) {
// ignore // ignore
} }
} }
@@ -78,7 +78,8 @@ function CMarketItem(appid, hashName, community, body, $) {
if (match) { if (match) {
try { try {
this.assets = JSON.parse(match[1]); this.assets = JSON.parse(match[1]);
this.assets = this.assets['730']['2']; this.assets = this.assets[appid];
this.assets = this.assets[Object.keys(this.assets)[0]];
this.firstAsset = this.assets[Object.keys(this.assets)[0]]; this.firstAsset = this.assets[Object.keys(this.assets)[0]];
} catch (e) { } catch (e) {
// ignore // ignore
@@ -90,7 +91,7 @@ function CMarketItem(appid, hashName, community, body, $) {
// TODO: Buying listings and placing buy orders // TODO: Buying listings and placing buy orders
} }
CMarketItem.prototype.updatePrice = function (currency, callback) { CMarketItem.prototype.updatePrice = function(currency, callback) {
if (this.commodity) { if (this.commodity) {
this.updatePriceForCommodity(currency, callback); this.updatePriceForCommodity(currency, callback);
} else { } else {
@@ -99,90 +100,88 @@ CMarketItem.prototype.updatePrice = function (currency, callback) {
}; };
CMarketItem.prototype.updatePriceForCommodity = function(currency, callback) { CMarketItem.prototype.updatePriceForCommodity = function(currency, callback) {
if(!this.commodity) { if (!this.commodity) {
throw new Error("Cannot update price for non-commodity item"); throw new Error('Cannot update price for non-commodity item');
} }
var self = this;
this._community.httpRequest({ this._community.httpRequest({
"uri": "https://steamcommunity.com/market/itemordershistogram?country=US&language=english&currency=" + currency + "&item_nameid=" + this.commodityID, url: 'https://steamcommunity.com/market/itemordershistogram?country=US&language=english&currency=' + currency + '&item_nameid=' + this.commodityID,
"json": true json: true
}, function(err, response, body) { }, (err, response, body) => {
if (err) {
callback(err);
return;
}
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);
}
self.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);
}
self.lowestPrice = parseInt(body.lowest_sell_order, 10);
self.highestBuyOrder = parseInt(body.highest_buy_order, 10);
// TODO: The tables?
if(callback) {
callback(null);
}
}, "steamcommunity");
};
CMarketItem.prototype.updatePriceForNonCommodity = function (currency, callback) {
if(this.commodity) {
throw new Error("Cannot update price for commodity item");
}
var self = this;
this._community.httpRequest({
"uri": "https://steamcommunity.com/market/listings/" +
this._appid + "/" +
encodeURIComponent(this._hashName) +
"/render/?query=&start=0&count=10&country=US&language=english&currency=" + currency,
"json": true
}, function(err, response, body) {
if (err) { if (err) {
callback(err); callback(err);
return; return;
} }
if (body.success != 1) { if (body.success != 1) {
callback && callback(new Error("Error " + body.success)); if (callback) {
callback(new Error('Error ' + body.success));
}
return; return;
} }
var match = body.total_count; let match = (body.sell_order_summary || '').match(/<span class="market_commodity_orders_header_promote">(\d+)<\/span>/);
if (match) { if (match) {
self.quantity = parseInt(match, 10); this.quantity = parseInt(match[1], 10);
} }
var lowestPrice; this.buyQuantity = 0;
var $ = Cheerio.load(body.results_html); match = (body.buy_order_summary || '').match(/<span class="market_commodity_orders_header_promote">(\d+)<\/span>/);
match = $(".market_listing_price.market_listing_price_with_fee");
if (match) { if (match) {
for (var i = 0; i < match.length; i++) { this.buyQuantity = parseInt(match[1], 10);
lowestPrice = parseFloat($(match[i]).text().replace(",", ".").replace(/[^\d.]/g, '')); }
this.lowestPrice = parseInt(body.lowest_sell_order, 10);
this.highestBuyOrder = parseInt(body.highest_buy_order, 10);
// TODO: The tables?
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&currency=' + 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)) { if (!isNaN(lowestPrice)) {
self.lowestPrice = lowestPrice; this.lowestPrice = lowestPrice;
break; break;
} }
} }
} }
callback && callback(null); callback && callback(null);
}, "steamcommunity"); }, 'steamcommunity');
}; };

View File

@@ -1,89 +1,89 @@
var SteamCommunity = require('../index.js'); const Cheerio = require('cheerio');
var Cheerio = require('cheerio');
const SteamCommunity = require('../index.js');
SteamCommunity.prototype.marketSearch = function(options, callback) { SteamCommunity.prototype.marketSearch = function(options, callback) {
var qs = {}; let qs = {};
if(typeof options === 'string') { if (typeof options === 'string') {
qs.query = options; qs.query = options;
} else { } else {
qs.query = options.query || ''; qs.query = options.query || '';
qs.appid = options.appid; qs.appid = options.appid;
qs.search_descriptions = options.searchDescriptions ? 1 : 0; qs.search_descriptions = options.searchDescriptions ? 1 : 0;
if(qs.appid) { if (qs.appid) {
for(var i in options) { for (let i in options) {
if(['query', 'appid', 'searchDescriptions'].indexOf(i) != -1) { if (['query', 'appid', 'searchDescriptions'].indexOf(i) != -1) {
continue; continue;
} }
// This is a tag // This is a tag
qs['category_' + qs.appid + '_' + i + '[]'] = 'tag_' + options[i]; qs['category_' + qs.appid + '_' + i + '[]'] = 'tag_' + options[i];
} }
} }
} }
qs.start = 0; qs.start = 0;
qs.count = 100; qs.count = 100;
qs.sort_column = 'price'; qs.sort_column = 'price';
qs.sort_dir = 'asc'; qs.sort_dir = 'asc';
var self = this; let results = [];
var results = []; const performSearch = () => {
performSearch(); this.httpRequest({
url: 'https://steamcommunity.com/market/search/render/',
function performSearch() { qs: qs,
self.httpRequest({ headers: {
"uri": "https://steamcommunity.com/market/search/render/", referer: 'https://steamcommunity.com/market/search'
"qs": qs,
"headers": {
"referer": "https://steamcommunity.com/market/search"
}, },
"json": true json: true
}, function(err, response, body) { }, (err, response, body) => {
if (err) { if (err) {
callback(err); callback(err);
return; return;
} }
if(!body.success) { if (!body.success) {
callback(new Error("Success is not true")); callback(new Error('Success is not true'));
return; return;
} }
if(!body.results_html) { if (!body.results_html) {
callback(new Error("No results_html in response")); callback(new Error('No results_html in response'));
return; return;
} }
var $ = Cheerio.load(body.results_html); let $ = Cheerio.load(body.results_html);
var $errorMsg = $('.market_listing_table_message'); let $errorMsg = $('.market_listing_table_message');
if($errorMsg.length > 0) { if ($errorMsg.length > 0) {
callback(new Error($errorMsg.text())); callback(new Error($errorMsg.text()));
return; return;
} }
var rows = $('.market_listing_row_link'); let rows = $('.market_listing_row_link');
for(var i = 0; i < rows.length; i++) { for (let i = 0; i < rows.length; i++) {
results.push(new CMarketSearchResult($(rows[i]))); results.push(new CMarketSearchResult($(rows[i])));
} }
if(body.start + body.pagesize >= body.total_count) { if (body.start + body.pagesize >= body.total_count) {
callback(null, results); callback(null, results);
} else { } else {
qs.start += body.pagesize; qs.start += body.pagesize;
performSearch(); performSearch();
} }
}, "steamcommunity"); }, 'steamcommunity');
} };
performSearch();
}; };
function CMarketSearchResult(row) { 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.appid = parseInt(match[1], 10);
this.market_hash_name = decodeURIComponent(match[2]); this.market_hash_name = decodeURIComponent(match[2]);
this.image = row.find('.market_listing_item_img').attr('src').match(/^https?:\/\/[^\/]+\/economy\/image\/[^\/]+\//)[0]; 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.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); this.quantity = parseInt(row.find('.market_listing_num_listings_qty').text().replace(/[^\d]+/g, ''), 10);
} }

View File

@@ -1,33 +1,33 @@
var SteamCommunity = require('../index.js'); const SteamID = require('steamid');
var Helpers = require('../components/helpers.js'); const XML2JS = require('xml2js');
var SteamID = require('steamid');
var xml2js = require('xml2js'); const Helpers = require('../components/helpers.js');
const SteamCommunity = require('../index.js');
SteamCommunity.prototype.getSteamGroup = function(id, callback) { SteamCommunity.prototype.getSteamGroup = function(id, callback) {
if(typeof id !== 'string' && !Helpers.isSteamID(id)) { if (typeof id !== 'string' && !Helpers.isSteamID(id)) {
throw new Error("id parameter should be a group URL string or a SteamID object"); 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)) { 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"); throw new Error('SteamID must stand for a clan account in the public universe');
} }
var self = this; this.httpRequest('https://steamcommunity.com/' + (typeof id === 'string' ? 'groups/' + id : 'gid/' + id.toString()) + '/memberslistxml/?xml=1', (err, response, body) => {
this.httpRequest("https://steamcommunity.com/" + (typeof id === 'string' ? "groups/" + id : "gid/" + id.toString()) + "/memberslistxml/?xml=1", function(err, response, body) {
if (err) { if (err) {
callback(err); callback(err);
return; return;
} }
xml2js.parseString(body, function(err, result) { XML2JS.parseString(body, (err, result) => {
if(err) { if (err) {
callback(err); callback(err);
return; return;
} }
callback(null, new CSteamGroup(self, result.memberList)); callback(null, new CSteamGroup(this, result.memberList));
}); });
}, "steamcommunity"); }, 'steamcommunity');
}; };
function CSteamGroup(community, groupData) { function CSteamGroup(community, groupData) {
@@ -49,16 +49,16 @@ CSteamGroup.prototype.getAvatarURL = function(size, protocol) {
size = size || ''; size = size || '';
protocol = protocol || 'http://'; protocol = protocol || 'http://';
var url = protocol + "steamcdn-a.akamaihd.net/steamcommunity/public/images/avatars/" + this.avatarHash.substring(0, 2) + "/" + this.avatarHash; let url = protocol + 'steamcdn-a.akamaihd.net/steamcommunity/public/images/avatars/' + this.avatarHash.substring(0, 2) + '/' + this.avatarHash;
if(size == 'full' || size == 'medium') { if (size == 'full' || size == 'medium') {
return url + "_" + size + ".jpg"; return url + '_' + size + '.jpg';
} else { } else {
return url + ".jpg"; return url + '.jpg';
} }
}; };
CSteamGroup.prototype.getMembers = function(addresses, callback) { CSteamGroup.prototype.getMembers = function(addresses, callback) {
if(typeof addresses === 'function') { if (typeof addresses === 'function') {
callback = addresses; callback = addresses;
addresses = null; addresses = null;
} }
@@ -78,16 +78,16 @@ CSteamGroup.prototype.getAllAnnouncements = function(time, callback) {
this._community.getAllGroupAnnouncements(this.steamID, time, callback); this._community.getAllGroupAnnouncements(this.steamID, time, callback);
}; };
CSteamGroup.prototype.postAnnouncement = function(headline, content, callback) { CSteamGroup.prototype.postAnnouncement = function(headline, content, hidden, callback) {
this._community.postGroupAnnouncement(this.steamID, headline, content, callback); this._community.postGroupAnnouncement(this.steamID, headline, content, hidden, callback);
}; };
CSteamGroup.prototype.editAnnouncement = function(annoucementID, headline, content, callback) { CSteamGroup.prototype.editAnnouncement = function(annoucementID, headline, content, callback) {
this._community.editGroupAnnouncement(this.steamID, annoucementID, headline, content, callback) this._community.editGroupAnnouncement(this.steamID, annoucementID, headline, content, callback);
}; };
CSteamGroup.prototype.deleteAnnouncement = function(annoucementID, callback) { CSteamGroup.prototype.deleteAnnouncement = function(annoucementID, callback) {
this._community.deleteGroupAnnouncement(this.steamID, annoucementID, callback) this._community.deleteGroupAnnouncement(this.steamID, annoucementID, callback);
}; };
CSteamGroup.prototype.scheduleEvent = function(name, type, description, time, server, callback) { CSteamGroup.prototype.scheduleEvent = function(name, type, description, time, server, callback) {
@@ -98,7 +98,7 @@ CSteamGroup.prototype.editEvent = function(id, name, type, description, time, se
this._community.editGroupEvent(this.steamID, 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) { CSteamGroup.prototype.deleteEvent = function(id, callback) {
this._community.deleteGroupEvent(this.steamID, id, callback); this._community.deleteGroupEvent(this.steamID, id, callback);
}; };
@@ -114,6 +114,19 @@ CSteamGroup.prototype.getHistory = function(page, callback) {
this._community.getGroupHistory(this.steamID, 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. * Get requests to join this restricted group.
* @param {function} callback - First argument is null/Error, second is array of SteamID objects * @param {function} callback - First argument is null/Error, second is array of SteamID objects

213
classes/CSteamSharedFile.js Normal file
View 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);
};

View File

@@ -1,57 +1,57 @@
var SteamCommunity = require('../index.js'); const SteamID = require('steamid');
var Helpers = require('../components/helpers.js'); const XML2JS = require('xml2js');
var SteamID = require('steamid');
var xml2js = require('xml2js'); const Helpers = require('../components/helpers.js');
const SteamCommunity = require('../index.js');
SteamCommunity.prototype.getSteamUser = function(id, callback) { SteamCommunity.prototype.getSteamUser = function(id, callback) {
if(typeof id !== 'string' && !Helpers.isSteamID(id)) { if (typeof id !== 'string' && !Helpers.isSteamID(id)) {
throw new Error("id parameter should be a user URL string or a SteamID object"); 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)) { 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"); throw new Error('SteamID must stand for an individual account in the public universe');
} }
var self = this; this.httpRequest('http://steamcommunity.com/' + (typeof id === 'string' ? 'id/' + id : 'profiles/' + id.toString()) + '/?xml=1', (err, response, body) => {
this.httpRequest("http://steamcommunity.com/" + (typeof id === 'string' ? "id/" + id : "profiles/" + id.toString()) + "/?xml=1", function(err, response, body) {
if (err) { if (err) {
callback(err); callback(err);
return; return;
} }
xml2js.parseString(body, function(err, result) { XML2JS.parseString(body, (err, result) => {
if(err || (!result.response && !result.profile)) { if (err || (!result.response && !result.profile)) {
callback(err || new Error("No valid response")); callback(err || new Error('No valid response'));
return; 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])); callback(new Error(result.response.error[0]));
return; return;
} }
// Try and find custom URL from redirect // Try and find custom URL from redirect
var customurl = null; let customurl = null;
if(response.request.redirects && response.request.redirects.length) { if (response.request.redirects && response.request.redirects.length) {
var match = response.request.redirects[0].redirectUri.match(/https?:\/\/steamcommunity\.com\/id\/([^/])+\/\?xml=1/); let match = response.request.redirects[0].redirectUri.match(/https?:\/\/steamcommunity\.com\/id\/([^/])+\/\?xml=1/);
if(match) { if (match) {
customurl = match[1]; customurl = match[1];
} }
} }
if(!result.profile.steamID64) { if (!result.profile.steamID64) {
callback(new Error("No valid response")); callback(new Error('No valid response'));
return; return;
} }
callback(null, new CSteamUser(self, result.profile, customurl)); callback(null, new CSteamUser(this, result.profile, customurl));
}); });
}, "steamcommunity"); }, 'steamcommunity');
}; };
function CSteamUser(community, userData, customurl) { function CSteamUser(community, userData, customurl) {
this._community = community; this._community = community;
this.steamID = new SteamID(userData.steamID64[0]); this.steamID = new SteamID(userData.steamID64[0]);
this.name = processItem('steamID'); this.name = processItem('steamID');
this.onlineState = processItem('onlineState'); this.onlineState = processItem('onlineState');
@@ -59,7 +59,7 @@ function CSteamUser(community, userData, customurl) {
this.privacyState = processItem('privacyState', 'uncreated'); this.privacyState = processItem('privacyState', 'uncreated');
this.visibilityState = processItem('visibilityState'); this.visibilityState = processItem('visibilityState');
this.avatarHash = processItem('avatarIcon', '').match(/([0-9a-f]+)\.[a-z]+$/); this.avatarHash = processItem('avatarIcon', '').match(/([0-9a-f]+)\.[a-z]+$/);
if(this.avatarHash) { if (this.avatarHash) {
this.avatarHash = this.avatarHash[1]; this.avatarHash = this.avatarHash[1];
} }
@@ -67,9 +67,15 @@ function CSteamUser(community, userData, customurl) {
this.tradeBanState = processItem('tradeBanState', 'None'); this.tradeBanState = processItem('tradeBanState', 'None');
this.isLimitedAccount = processItem('isLimitedAccount') == 1; this.isLimitedAccount = processItem('isLimitedAccount') == 1;
this.customURL = processItem('customURL', customurl); this.customURL = processItem('customURL', customurl);
if(this.visibilityState == 3) { if (this.visibilityState == SteamCommunity.PrivacyState.Public) {
this.memberSince = new Date(processItem('memberSince', '0').replace(/(\d{1,2})(st|nd|th)/, "$1")); 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.location = processItem('location');
this.realName = processItem('realname'); this.realName = processItem('realname');
this.summary = processItem('summary'); this.summary = processItem('summary');
@@ -79,25 +85,24 @@ function CSteamUser(community, userData, customurl) {
this.realName = null; this.realName = null;
this.summary = null; this.summary = null;
} }
// Maybe handle mostPlayedGames? // Maybe handle mostPlayedGames?
this.groups = null; this.groups = null;
this.primaryGroup = null; this.primaryGroup = null;
var self = this; if (userData.groups && userData.groups[0] && userData.groups[0].group) {
if(userData.groups && userData.groups[0] && userData.groups[0].group) { this.groups = userData.groups[0].group.map((group) => {
this.groups = userData.groups[0].group.map(function(group) { if (group.$ && group.$.isPrimary === '1') {
if(group['$'] && group['$'].isPrimary === "1") { this.primaryGroup = new SteamID(group.groupID64[0]);
self.primaryGroup = new SteamID(group.groupID64[0]);
} }
return new SteamID(group.groupID64[0]); return new SteamID(group.groupID64[0]);
}); });
} }
function processItem(name, defaultVal) { function processItem(name, defaultVal) {
if(!userData[name]) { if (!userData[name]) {
return defaultVal; return defaultVal;
} }
@@ -109,13 +114,13 @@ CSteamUser.getAvatarURL = function(hash, size, protocol) {
size = size || ''; size = size || '';
protocol = protocol || 'http://'; protocol = protocol || 'http://';
hash = hash || "72f78b4c8cc1f62323f8a33f6d53e27db57c2252"; // The default "?" avatar hash = hash || '72f78b4c8cc1f62323f8a33f6d53e27db57c2252'; // The default "?" avatar
var url = protocol + "steamcdn-a.akamaihd.net/steamcommunity/public/images/avatars/" + hash.substring(0, 2) + "/" + hash; let url = protocol + 'steamcdn-a.akamaihd.net/steamcommunity/public/images/avatars/' + hash.substring(0, 2) + '/' + hash;
if(size == 'full' || size == 'medium') { if (size == 'full' || size == 'medium') {
return url + "_" + size + ".jpg"; return url + '_' + size + '.jpg';
} else { } else {
return url + ".jpg"; return url + '.jpg';
} }
}; };
@@ -148,6 +153,14 @@ CSteamUser.prototype.comment = function(message, callback) {
this._community.postUserComment(this.steamID, message, callback); 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) { CSteamUser.prototype.inviteToGroup = function(groupID, callback) {
this._community.inviteUserToGroup(this.steamID, groupID, callback); this._community.inviteUserToGroup(this.steamID, groupID, callback);
}; };
@@ -177,8 +190,27 @@ CSteamUser.prototype.getInventory = function(appID, contextID, tradableOnly, cal
* @param {int} appID - The Steam application ID of the game for which you want an inventory * @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 {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 {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 * @param callback
*/ */
CSteamUser.prototype.getInventoryContents = function(appID, contextID, tradableOnly, callback) { CSteamUser.prototype.getInventoryContents = function(appID, contextID, tradableOnly, language, callback) {
this._community.getUserInventoryContents(this.steamID, appID, contextID, tradableOnly, 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);
}; };

View File

@@ -1,258 +0,0 @@
var SteamCommunity = require('../index.js');
var SteamID = require('steamid');
SteamCommunity.ChatState = require('../resources/EChatState.js');
SteamCommunity.PersonaState = require('../resources/EPersonaState.js');
SteamCommunity.PersonaStateFlag = require('../resources/EPersonaStateFlag.js');
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.getWebApiOauthToken(function(err, token) {
if(err) {
var fatal = err.message.indexOf('not authorized') != -1;
if (!fatal) {
self.chatState = SteamCommunity.ChatState.LogOnFailed;
setTimeout(self.chatLogon.bind(self), 5000);
} else {
self.chatState = SteamCommunity.ChatState.Offline;
}
self.emit('chatLogOnFailed', err, fatal);
self.emit('debug', "Cannot get oauth token: " + err.message);
return;
}
self.httpRequestPost({
"uri": "https://api.steampowered.com/ISteamWebUserPresenceOAuth/Logon/v1",
"form": {
"ui_mode": uiMode,
"access_token": token
},
"json": true
}, function(err, response, body) {
if(err || response.statusCode != 200) {
self.chatState = SteamCommunity.ChatState.LogOnFailed;
self.emit('chatLogOnFailed', err ? err : new Error("HTTP error " + response.statusCode), false);
self.emit('debug', 'Error logging into webchat: ' + (err ? err.message : "HTTP error " + response.statusCode));
setTimeout(self.chatLogon.bind(self), 5000);
return;
}
if(body.error != 'OK') {
self.chatState = SteamCommunity.ChatState.LogOnFailed;
self.emit('chatLogOnFailed', new Error(body.error), false);
self.emit('debug', 'Error logging into webchat: ' + body.error);
setTimeout(self.chatLogon.bind(self), 5000);
return;
}
self._chat = {
"umqid": body.umqid,
"message": body.message,
"accessToken": token,
"interval": interval,
"uiMode": uiMode
};
self.chatFriends = {};
self.chatState = SteamCommunity.ChatState.LoggedOn;
self.emit('chatLoggedOn');
self._chatPoll();
}, "steamcommunity");
});
};
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';
var self = this;
this.httpRequestPost({
"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 (err) {
callback(err);
return;
}
if(body.error != 'OK') {
callback(new Error(body.error));
} else {
callback(null);
}
}, "steamcommunity");
};
SteamCommunity.prototype.chatLogoff = function() {
var self = this;
this.httpRequestPost({
"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");
};
SteamCommunity.prototype._chatPoll = function() {
this.emit('debug', 'Doing chat poll');
var self = this;
this.httpRequestPost({
"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));
if (err.message == "Not Logged On") {
self._relogWebChat();
}
return;
}
if(!body || body.error != 'OK') {
self.emit('debug', 'Error in chat poll: ' + (body && body.error ? body.error : "Malformed response"));
if (body && body.error && body.error == "Not Logged On") {
self._relogWebChat();
}
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");
};
SteamCommunity.prototype._relogWebChat = function() {
this.emit('debug', "Relogging web chat");
clearTimeout(this._chat.timer);
this.chatState = SteamCommunity.ChatState.Offline;
this.chatLogon(this._chat.interval, this._chat.uiMode);
};
SteamCommunity.prototype._chatUpdatePersona = function(steamID) {
if (!this.chatFriends || this.chatState == SteamCommunity.ChatState.Offline) {
return; // we no longer care
}
this.emit('debug', 'Updating persona data for ' + steamID);
var self = this;
this.httpRequest({
"uri": "https://steamcommunity.com/chat/friendstate/" + steamID.accountid,
"json": true
}, function(err, response, body) {
if (!self.chatFriends || self.chatState == SteamCommunity.ChatState.Offline) {
return; // welp
}
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;
}, "steamcommunity");
};

View File

@@ -1,104 +1,79 @@
var SteamCommunity = require('../index.js'); const Cheerio = require('cheerio');
var Cheerio = require('cheerio'); const StdLib = require('@doctormckay/stdlib');
var SteamTotp = require('steam-totp'); const SteamTotp = require('steam-totp');
var Async = require('async');
var CConfirmation = require('../classes/CConfirmation.js'); 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. * Get a list of your account's currently outstanding confirmations.
* @param {int} time - The unix timestamp with which the following key was generated * @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 {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 * @param {SteamCommunity~getConfirmations} [callback] - Called when the list of confirmations is received
* @return Promise<{confirmations: CConfirmation[]}>
*/ */
SteamCommunity.prototype.getConfirmations = function(time, key, callback) { SteamCommunity.prototype.getConfirmations = function(time, key, callback) {
var self = this; return StdLib.Promises.callbackPromise(['confirmations'], callback, false, async (resolve, reject) => {
let body = await request(this, 'getlist', key, time, 'list', null);
request(this, "conf", key, time, "conf", null, false, function(err, body) { if (!body.success) {
if(err) { if (body.needauth) {
if (err.message == "Invalid protocol: steammobile:") { let err = new Error('Not Logged In');
err.message = "Not Logged In"; this._notifySessionExpired(err);
self._notifySessionExpired(err); return reject(err);
} }
callback(err); return reject(new Error(body.message || body.detail || 'Failed to get confirmation list'));
return;
} }
var $ = Cheerio.load(body); let confs = (body.conf || []).map(conf => new CConfirmation(this, {
var empty = $('#mobileconf_empty'); id: conf.id,
if(empty.length > 0) { type: conf.type,
if(!$(empty).hasClass('mobileconf_done')) { creator: conf.creator_id,
// An error occurred key: conf.nonce,
callback(new Error(empty.find('div:nth-of-type(2)').text())); title: `${conf.type_name || 'Confirm'} - ${conf.headline || ''}`,
} else { receiving: conf.type == EConfirmationType.Trade ? ((conf.summary || [])[1] || '') : '',
callback(null, []); 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 || ''
}));
return; resolve({confirmations: confs});
}
// We have something to confirm
var confirmations = $('#mobileconf_list');
if(!confirmations) {
callback(new Error("Malformed response"));
return;
}
var confs = [];
Array.prototype.forEach.call(confirmations.find('.mobileconf_list_entry'), function(conf) {
conf = $(conf);
var img = conf.find('.mobileconf_list_entry_icon img');
confs.push(new CConfirmation(self, {
"id": conf.data('confid'),
"type": conf.data('type'),
"creator": conf.data('creator'),
"key": conf.data('key'),
"title": conf.find('.mobileconf_list_entry_description>div:nth-of-type(1)').text().trim(),
"receiving": conf.find('.mobileconf_list_entry_description>div:nth-of-type(2)').text().trim(),
"time": conf.find('.mobileconf_list_entry_description>div:nth-of-type(3)').text().trim(),
"icon": img.length < 1 ? '' : $(img).attr('src')
}));
});
callback(null, confs);
}); });
}; };
/** /**
* @callback SteamCommunity~getConfirmations * @callback SteamCommunity~getConfirmations
* @param {Error|null} err - An Error object on failure, or null on success * @param {Error|null} err - An Error object on failure, or null on success
* @param {CConfirmation[]} confirmations - An array of CConfirmation objects * @param {CConfirmation[]} [confirmations] - An array of CConfirmation objects
*/ */
/** /**
* Get the trade offer ID associated with a particular confirmation * Get the trade offer ID associated with a particular confirmation
* @param {int} confID - The ID of the confirmation in question * @param {int} confID - The ID of the confirmation in question
* @param {int} time - The unix timestamp with which the following key was generated * @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 "details" (this key can be reused) * @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 * @param {SteamCommunity~getConfirmationOfferID} [callback]
* @return Promise<{offerID: string|null}>
*/ */
SteamCommunity.prototype.getConfirmationOfferID = function(confID, time, key, callback) { SteamCommunity.prototype.getConfirmationOfferID = function(confID, time, key, callback) {
request(this, "details/" + confID, key, time, "details", null, true, function(err, body) { return StdLib.Promises.callbackPromise(['offerID'], callback, false, async (resolve, reject) => {
if(err) { let body = await request(this, 'detailspage/' + confID, key, time, 'detail', null);
callback(err);
return; if (typeof body != 'string') {
return reject(new Error('Cannot load confirmation details'));
} }
if(!body.success) { let $ = Cheerio.load(body);
callback(new Error("Cannot load confirmation details")); let offer = $('.tradeoffer');
return; if (offer.length < 1) {
return resolve({offerID: null});
} }
var $ = Cheerio.load(body.html); resolve({offerID: offer.attr('id').split('_')[1]});
var offer = $('.tradeoffer');
if(offer.length < 1) {
callback(null, null);
return;
}
callback(null, offer.attr('id').split('_')[1]);
}); });
}; };
@@ -110,39 +85,30 @@ SteamCommunity.prototype.getConfirmationOfferID = function(confID, time, key, ca
/** /**
* Confirm or cancel a given confirmation. * Confirm or cancel a given confirmation.
* @param {int|int[]} confID - The ID of the confirmation in question, or an array of confirmation IDs * @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 {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 {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 {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 {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 * @param {SteamCommunity~genericErrorCallback} [callback] - Called when the request is complete
* @return Promise<void>
*/ */
SteamCommunity.prototype.respondToConfirmation = function(confID, confKey, time, key, accept, callback) { SteamCommunity.prototype.respondToConfirmation = function(confID, confKey, time, key, accept, callback) {
request(this, (confID instanceof Array) ? "multiajaxop" : "ajaxop", key, time, accept ? "allow" : "cancel", { return StdLib.Promises.callbackPromise(null, callback, true, async (resolve, reject) => {
"op": accept ? "allow" : "cancel", let tag = accept ? 'accept' : 'reject';
"cid": confID,
"ck": confKey // The official app uses tags reject/accept, but cancel/allow still works so use these for backward compatibility
}, true, function(err, body) { let body = await request(this, (confID instanceof Array) ? 'multiajaxop' : 'ajaxop', key, time, tag, {
if(!callback) { op: accept ? 'allow' : 'cancel',
return; cid: confID,
ck: confKey
});
if (body.success) {
return resolve();
} }
if(err) { reject(new Error(body.message || body.detail || 'Could not act on confirmation'));
callback(err);
return;
}
if(body.success) {
callback(null);
return;
}
if(body.message) {
callback(new Error(body.message));
return;
}
callback(new Error("Could not act on confirmation"));
}); });
}; };
@@ -150,101 +116,81 @@ SteamCommunity.prototype.respondToConfirmation = function(confID, confKey, time,
* Accept a confirmation for a given object (trade offer or market listing) automatically. * Accept a confirmation for a given object (trade offer or market listing) automatically.
* @param {string} identitySecret * @param {string} identitySecret
* @param {number|string} objectID * @param {number|string} objectID
* @param {SteamCommunity~genericErrorCallback} callback * @param {SteamCommunity~genericErrorCallback} [callback]
* @return Promise<void>
*/ */
SteamCommunity.prototype.acceptConfirmationForObject = function(identitySecret, objectID, callback) { SteamCommunity.prototype.acceptConfirmationForObject = function(identitySecret, objectID, callback) {
var self = this;
this._usedConfTimes = this._usedConfTimes || []; this._usedConfTimes = this._usedConfTimes || [];
if (typeof this._timeOffset !== 'undefined') { return StdLib.Promises.callbackPromise(null, callback, true, async (resolve, reject) => {
// time offset is already known and saved // Figure out our time offset
doConfirmation(); if (typeof this._timeOffset == 'undefined') {
} else { await new Promise((resolve) => {
SteamTotp.getTimeOffset(function(err, offset) { SteamTotp.getTimeOffset((err, offset) => {
if (err) { if (err) {
callback(err); // not critical that this succeeds
return; return resolve();
} }
self._timeOffset = offset; this._timeOffset = offset;
doConfirmation(); resolve();
});
});
}
setTimeout(function() { let offset = this._timeOffset;
// Delete the saved time offset after 12 hours because why not let time = SteamTotp.time(offset);
delete self._timeOffset; let key = SteamTotp.getConfirmationKey(identitySecret, time, 'list');
}, 1000 * 60 * 60 * 12); let {confirmations} = await this.getConfirmations(time, key);
});
}
function doConfirmation() { let conf = confirmations.find(conf => conf.creator == objectID);
var offset = self._timeOffset; if (!conf) {
var time = SteamTotp.time(offset); return reject(new Error(`Could not find confirmation for object ${objectID}`));
self.getConfirmations(time, SteamTotp.getConfirmationKey(identitySecret, time, "conf"), function(err, confs) { }
if (err) {
callback(err);
return;
}
var conf = confs.filter(function(conf) { return conf.creator == objectID; }); // make sure we don't reuse the same time
if (conf.length == 0) { let localOffset = 0;
callback(new Error("Could not find confirmation for object " + objectID)); do {
return; time = SteamTotp.time(offset) + localOffset++;
} } while (this._usedConfTimes.includes(time));
conf = conf[0]; 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
}
// make sure we don't reuse the same time await conf.respond(time, SteamTotp.getConfirmationKey(identitySecret, time, 'accept'), true);
var localOffset = 0; });
do {
time = SteamTotp.time(offset) + localOffset++;
} while (self._usedConfTimes.indexOf(time) != -1);
self._usedConfTimes.push(time);
if (self._usedConfTimes.length > 60) {
self._usedConfTimes.splice(0, self._usedConfTimes.length - 60); // we don't need to save more than 60 entries
}
conf.respond(time, SteamTotp.getConfirmationKey(identitySecret, time, "allow"), true, callback);
});
}
}; };
/** /**
* Send a single request to Steam to accept all outstanding confirmations (after loading the list). If one fails, the * 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. * entire request will fail and there will be no way to know which failed without loading the list again.
* @param {number} time * @param {number} time
* @param {string} confKey * @param {string} listKey
* @param {string} allowKey * @param {string} acceptKey
* @param {function} callback * @param {function} [callback]
* @return Promise<{confirmations: CConfirmation[]}>
*/ */
SteamCommunity.prototype.acceptAllConfirmations = function(time, confKey, allowKey, callback) { SteamCommunity.prototype.acceptAllConfirmations = function(time, listKey, acceptKey, callback) {
var self = this; return StdLib.Promises.callbackPromise(null, callback, true, async (resolve, reject) => {
let {confirmations} = await this.getConfirmations(time, listKey);
this.getConfirmations(time, confKey, function(err, confs) { if (confirmations.length == 0) {
if (err) { return resolve({confirmations: []});
callback(err);
return;
} }
if (confs.length == 0) { let confIds = confirmations.map(conf => conf.id);
callback(null, []); let confKeys = confirmations.map(conf => conf.key);
return; await this.respondToConfirmation(confIds, confKeys, time, acceptKey, true);
}
self.respondToConfirmation(confs.map(function(conf) { return conf.id; }), confs.map(function(conf) { return conf.key; }), time, allowKey, true, function(err) { resolve({confirmations});
if (err) {
callback(err);
return;
}
callback(err, confs);
});
}); });
}; };
function request(community, url, key, time, tag, params, json, callback) { async function request(community, url, key, time, tag, params) {
if (!community.steamID) { if (!community.steamID) {
throw new Error("Must be logged in before trying to do anything with confirmations"); throw new Error('Must be logged in before trying to do anything with confirmations');
} }
params = params || {}; params = params || {};
@@ -252,174 +198,21 @@ function request(community, url, key, time, tag, params, json, callback) {
params.a = community.steamID.getSteamID64(); params.a = community.steamID.getSteamID64();
params.k = key; params.k = key;
params.t = time; params.t = time;
params.m = "android"; params.m = 'react';
params.tag = tag; params.tag = tag;
var req = { let req = {
"method": url == 'multiajaxop' ? 'POST' : 'GET', method: url == 'multiajaxop' ? 'POST' : 'GET',
"uri": "https://steamcommunity.com/mobileconf/" + url, url: `https://steamcommunity.com/mobileconf/${url}`,
"json": !!json source: 'steamcommunity'
}; };
if (req.method == "GET") { if (req.method == 'GET') {
req.qs = params; req.qs = params;
} else { } else {
req.form = params; req.form = params;
} }
community.httpRequest(req, function(err, response, body) { let result = await community.httpRequest(req);
if (err) { return result.jsonBody || result.textBody;
callback(err);
return;
}
callback(null, body);
}, "steamcommunity");
} }
// Confirmation checker
/**
* Start automatically polling our confirmations for new ones. The `confKeyNeeded` event will be emitted when we need a confirmation key, or `newConfirmation` when we get a new confirmation
* @param {int} pollInterval - The interval, in milliseconds, at which we will poll for confirmations. This should probably be at least 10,000 to avoid rate-limits.
* @param {Buffer|string|null} [identitySecret=null] - Your identity_secret. If passed, all confirmations will be automatically accepted and nothing will be emitted.
*/
SteamCommunity.prototype.startConfirmationChecker = function(pollInterval, identitySecret) {
this._confirmationPollInterval = pollInterval;
this._knownConfirmations = this._knownConfirmations || {};
this._confirmationKeys = this._confirmationKeys || {};
this._identitySecret = identitySecret;
if(this._confirmationTimer) {
clearTimeout(this._confirmationTimer);
}
setTimeout(this.checkConfirmations.bind(this), 500);
};
/**
* Stop automatically polling our confirmations.
*/
SteamCommunity.prototype.stopConfirmationChecker = function() {
if(this._confirmationPollInterval) {
delete this._confirmationPollInterval;
}
if(this._identitySecret) {
delete this._identitySecret;
}
if(this._confirmationTimer) {
clearTimeout(this._confirmationTimer);
delete this._confirmationTimer;
}
};
/**
* Run the confirmation checker right now instead of waiting for the next poll.
* Useful to call right after you send/accept an offer that needs confirmation.
*/
SteamCommunity.prototype.checkConfirmations = function() {
if(this._confirmationTimer) {
clearTimeout(this._confirmationTimer);
delete this._confirmationTimer;
}
var self = this;
if(!this._confirmationQueue) {
this._confirmationQueue = Async.queue(function(conf, callback) {
// Worker to process new confirmations
if(self._identitySecret) {
// We should accept this
self.emit('debug', "Accepting confirmation #" + conf.id);
var time = Math.floor(Date.now() / 1000);
conf.respond(time, SteamTotp.getConfirmationKey(self._identitySecret, time, "allow"), true, function(err) {
// If there was an error and it wasn't actually accepted, we'll pick it up again
if (!err) self.emit('confirmationAccepted', conf);
delete self._knownConfirmations[conf.id];
setTimeout(callback, 1000); // Call the callback in 1 second, to make sure the time changes
});
} else {
self.emit('newConfirmation', conf);
setTimeout(callback, 1000); // Call the callback in 1 second, to make sure the time changes
}
}, 1);
}
this.emit('debug', 'Checking confirmations');
this._confirmationCheckerGetKey('conf', function(err, key) {
if(err) {
resetTimer();
return;
}
self.getConfirmations(key.time, key.key, function(err, confirmations) {
if(err) {
self.emit('debug', "Can't check confirmations: " + err.message);
resetTimer();
return;
}
var known = self._knownConfirmations;
var newOnes = confirmations.filter(function(conf) {
return !known[conf.id];
});
if(newOnes.length < 1) {
resetTimer();
return; // No new ones
}
// We have new confirmations!
newOnes.forEach(function(conf) {
self._knownConfirmations[conf.id] = conf; // Add it to our list of known confirmations
self._confirmationQueue.push(conf);
});
resetTimer();
});
});
function resetTimer() {
if(self._confirmationPollInterval) {
self._confirmationTimer = setTimeout(self.checkConfirmations.bind(self), self._confirmationPollInterval);
}
}
};
SteamCommunity.prototype._confirmationCheckerGetKey = function(tag, callback) {
if(this._identitySecret) {
if(tag == 'details') {
// We don't care about details
callback(new Error("Disabled"));
return;
}
var time = Math.floor(Date.now() / 1000);
callback(null, {"time": time, "key": SteamTotp.getConfirmationKey(this._identitySecret, time, tag)});
return;
}
var existing = this._confirmationKeys[tag];
var reusable = ['conf', 'details'];
// See if we already have a key that we can reuse.
if(reusable.indexOf(tag) != -1 && existing && (Date.now() - (existing.time * 1000) < (1000 * 60 * 5))) {
callback(null, existing);
return;
}
// We need a fresh one
var self = this;
this.emit('confKeyNeeded', tag, function(err, time, key) {
if(err) {
callback(err);
return;
}
self._confirmationKeys[tag] = {"time": time, "key": key};
callback(null, {"time": time, "key": key});
});
};

View File

@@ -1,68 +1,69 @@
var SteamCommunity = require('../index.js'); const Cheerio = require('cheerio');
var SteamID = require('steamid'); const SteamID = require('steamid');
var xml2js = require('xml2js'); const XML2JS = require('xml2js');
var Cheerio = require('cheerio');
var EResult = SteamCommunity.EResult; const Helpers = require('./helpers.js');
const SteamCommunity = require('../index.js');
const EResult = SteamCommunity.EResult;
SteamCommunity.prototype.getGroupMembers = function(gid, callback, members, link, addresses, addressIdx) { SteamCommunity.prototype.getGroupMembers = function(gid, callback, members, link, addresses, addressIdx) {
members = members || []; members = members || [];
if (!link) { if (!link) {
if (typeof gid !== 'string') { if (typeof gid != 'string') {
// It's a SteamID object // It's a SteamID object
link = "http://steamcommunity.com/gid/" + gid.toString() + "/memberslistxml/?xml=1"; link = `https://steamcommunity.com/gid/${gid.toString()}/memberslistxml/?xml=1`;
} else { } else {
try { try {
var sid = new SteamID(gid); // 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()) { if (sid.type == SteamID.Type.CLAN && sid.isValid()) {
link = "http://steamcommunity.com/gid/" + sid.getSteamID64() + "/memberslistxml/?xml=1"; link = `https://steamcommunity.com/gid/${sid.getSteamID64()}/memberslistxml/?xml=1`;
} else { } else {
throw new Error("Doesn't particularly matter what this message is"); throw new Error('Doesn\'t particularly matter what this message is');
} }
} catch (e) { } catch (e) {
link = "http://steamcommunity.com/groups/" + gid + "/memberslistxml/?xml=1"; link = `http://steamcommunity.com/groups/${gid}/memberslistxml/?xml=1`;
} }
} }
} }
addressIdx = addressIdx || 0; addressIdx = addressIdx || 0;
var options = {}; let options = {};
options.uri = link; options.uri = link;
if(addresses) { if (addresses) {
if(addressIdx >= addresses.length) { if (addressIdx >= addresses.length) {
addressIdx = 0; addressIdx = 0;
} }
options.localAddress = addresses[addressIdx]; options.localAddress = addresses[addressIdx];
} }
var self = this; this.httpRequest(options, (err, response, body) => {
this.httpRequest(options, function(err, response, body) {
if (err) { if (err) {
callback(err); callback(err);
return; return;
} }
xml2js.parseString(body, function(err, result) { XML2JS.parseString(body, (err, result) => {
if (err) { if (err) {
callback(err); callback(err);
return; return;
} }
members = members.concat(result.memberList.members[0].steamID64.map(function(steamID) { members = members.concat(result.memberList.members[0].steamID64.map(sid => new SteamID(sid)));
return new SteamID(steamID);
}));
if (result.memberList.nextPageLink) { if (result.memberList.nextPageLink) {
addressIdx++; addressIdx++;
self.getGroupMembers(gid, callback, members, result.memberList.nextPageLink[0], addresses, addressIdx); this.getGroupMembers(gid, callback, members, result.memberList.nextPageLink[0], addresses, addressIdx);
} else { } else {
callback(null, members); callback(null, members);
} }
}); });
}, "steamcommunity"); }, 'steamcommunity');
}; };
SteamCommunity.prototype.getGroupMembersEx = function(gid, addresses, callback) { SteamCommunity.prototype.getGroupMembersEx = function(gid, addresses, callback) {
@@ -70,38 +71,36 @@ SteamCommunity.prototype.getGroupMembersEx = function(gid, addresses, callback)
}; };
SteamCommunity.prototype.joinGroup = function(gid, callback) { SteamCommunity.prototype.joinGroup = function(gid, callback) {
if(typeof gid === 'string') { if (typeof gid == 'string') {
gid = new SteamID(gid); gid = new SteamID(gid);
} }
var self = this;
this.httpRequestPost({ this.httpRequestPost({
"uri": "https://steamcommunity.com/gid/" + gid.getSteamID64(), url: `https://steamcommunity.com/gid/${gid.getSteamID64()}`,
"form": { form: {
"action": "join", action: 'join',
"sessionID": this.getSessionID() sessionID: this.getSessionID()
} }
}, function(err, response, body) { }, (err, response, body) => {
if(!callback) { if (!callback) {
return; return;
} }
callback(err || null); callback(err || null);
}, "steamcommunity"); }, 'steamcommunity');
}; };
SteamCommunity.prototype.leaveGroup = function(gid, callback) { SteamCommunity.prototype.leaveGroup = function(gid, callback) {
if(typeof gid === 'string') { if (typeof gid == 'string') {
gid = new SteamID(gid); gid = new SteamID(gid);
} }
var self = this; this._myProfile('home_process', {
this._myProfile("home_process", { sessionID: this.getSessionID(),
"sessionID": this.getSessionID(), action: 'leaveGroup',
"action": "leaveGroup", groupId: gid.getSteamID64()
"groupId": gid.getSteamID64() }, (err, response, body) => {
}, function(err, response, body) { if (!callback) {
if(!callback) {
return; return;
} }
@@ -110,212 +109,164 @@ SteamCommunity.prototype.leaveGroup = function(gid, callback) {
}; };
SteamCommunity.prototype.getAllGroupAnnouncements = function(gid, time, callback) { SteamCommunity.prototype.getAllGroupAnnouncements = function(gid, time, callback) {
if(typeof gid === 'string') { if (typeof gid == 'string') {
gid = new SteamID(gid); gid = new SteamID(gid);
} }
if(typeof time === 'function') { if (typeof time == 'function') {
callback = time; callback = time;
time = new Date(0); // The beginnig of time... time = new Date(0); // The beginnig of time...
} }
var self = this;
this.httpRequest({ this.httpRequest({
"uri": "https://steamcommunity.com/gid/" + gid.getSteamID64() + "/rss/" url: `https://steamcommunity.com/gid/${gid.getSteamID64()}/rss/`
}, function(err, response, body) { }, (err, response, body) => {
if (err) { if (err) {
callback(err); callback(err);
return; return;
} }
xml2js.parseString(body, function(err, results) { XML2JS.parseString(body, (err, results) => {
if(err) { if (err) {
return callback(err); return callback(err);
} }
if(!results.rss.channel[0].item) { if (!results.rss.channel[0].item) {
return callback(null, []); return callback(null, []);
} }
var announcements = results.rss.channel[0].item.map(function(announcement) { let announcements = results.rss.channel[0].item.map((announcement) => {
var splitLink = announcement.link[0].split('/'); let splitLink = announcement.link[0].split('/');
return { return {
headline: announcement.title[0], headline: announcement.title[0],
content: announcement.description[0], content: announcement.description[0],
date: new Date(announcement.pubDate[0]), date: new Date(announcement.pubDate[0]),
author: (typeof announcement.author === 'undefined') ? null : announcement.author[0], author: (typeof announcement.author === 'undefined') ? null : announcement.author[0],
aid: splitLink[splitLink.length - 1] aid: splitLink[splitLink.length - 1]
} };
}).filter(function(announcement) { }).filter(announcement => announcement.date > time);
return (announcement.date > time);
});
return callback(null, announcements); return callback(null, announcements);
}); });
}, "steamcommunity"); }, 'steamcommunity');
}; };
SteamCommunity.prototype.postGroupAnnouncement = function(gid, headline, content, callback) { SteamCommunity.prototype.postGroupAnnouncement = function(gid, headline, content, hidden, callback) {
if(typeof gid === 'string') { if (typeof gid == 'string') {
gid = new SteamID(gid); gid = new SteamID(gid);
} }
var self = this; 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({ this.httpRequestPost({
"uri": "https://steamcommunity.com/gid/" + gid.getSteamID64() + "/announcements", url: `https://steamcommunity.com/gid/${gid.getSteamID64()}/announcements`,
"form": { form
"sessionID": this.getSessionID(), }, (err, response, body) => {
"action": "post", if (!callback) {
"headline": headline,
"body": content,
"languages[0][headline]": headline,
"languages[0][body]": content
}
}, function(err, response, body) {
if(!callback) {
return; return;
} }
callback(err || null); callback(err || null);
}, "steamcommunity"); }, 'steamcommunity');
}; };
SteamCommunity.prototype.editGroupAnnouncement = function(gid, aid, headline, content, callback) { SteamCommunity.prototype.editGroupAnnouncement = function(gid, aid, headline, content, callback) {
if(typeof gid === 'string') { if (typeof gid == 'string') {
gid = new SteamID(gid); gid = new SteamID(gid);
} }
var self = this; let submitData = {
url: `https://steamcommunity.com/gid/${gid.getSteamID64()}/announcements`,
var submitData = { form: {
"uri": "https://steamcommunity.com/gid/" + gid.getSteamID64() + "/announcements", sessionID: this.getSessionID(),
"form": { gid: aid,
"sessionID": this.getSessionID(), action: 'update',
"gid": aid, headline: headline,
"action": "update", body: content,
"headline": headline, 'languages[0][headline]': headline,
"body": content, 'languages[0][body]': content,
"languages[0][headline]": headline, 'languages[0][updated]': 1
"languages[0][body]": content,
"languages[0][updated]": 1
} }
}; };
this.httpRequestPost(submitData, function(err, response, body) { this.httpRequestPost(submitData, (err, response, body) => {
if(!callback) { if (!callback) {
return; return;
} }
callback(err || null); callback(err || null);
}, "steamcommunity"); }, 'steamcommunity');
}; };
SteamCommunity.prototype.deleteGroupAnnouncement = function(gid, aid, callback) { SteamCommunity.prototype.deleteGroupAnnouncement = function(gid, aid, callback) {
if(typeof gid === 'string') { if (typeof gid == 'string') {
gid = new SteamID(gid); gid = new SteamID(gid);
} }
var self = this; let submitData = {
url: `https://steamcommunity.com/gid/${gid.getSteamID64()}/announcements/delete/${aid}?sessionID=${this.getSessionID()}`
var submitData = {
"uri": "https://steamcommunity.com/gid/" + gid.getSteamID64() + "/announcements/delete/" + aid + "?sessionID=" + this.getSessionID()
}; };
this.httpRequestGet(submitData, function(err, response, body) { this.httpRequestGet(submitData, (err, response, body) => {
if(!callback) { if (!callback) {
return; return;
} }
callback(err || null); callback(err || null);
}, "steamcommunity"); }, 'steamcommunity');
}; };
SteamCommunity.prototype.scheduleGroupEvent = function(gid, name, type, description, time, server, callback) { SteamCommunity.prototype.scheduleGroupEvent = function(gid, name, type, description, time, server, callback) {
if(typeof gid === 'string') { if (typeof gid == 'string') {
gid = new SteamID(gid); 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 // 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 // Passing a number for type will make it a game event for that appid
if(typeof server === 'function') { switch (typeof server) {
callback = server; case 'function':
server = {"ip": "", "password": ""}; callback = server;
} else if(typeof server === 'string') { server = {ip: '', password: ''};
server = {"ip": server, "password": ""}; break;
} else if(typeof server !== 'object') {
server = {"ip": "", "password": ""}; case 'string':
server = {ip: server, password: ''};
break;
default:
if (typeof server != 'object') {
server = {ip: '', password: ''};
}
} }
var form = { let form = {
"sessionid": this.getSessionID(), sessionid: this.getSessionID(),
"action": "newEvent", action: 'newEvent',
"tzOffset": new Date().getTimezoneOffset() * -60, tzOffset: new Date().getTimezoneOffset() * -60,
"name": name, name: name,
"type": (typeof type === 'number' || !isNaN(parseInt(type, 10)) ? "GameEvent" : type), type: (typeof type == 'number' || !isNaN(parseInt(type, 10)) ? 'GameEvent' : type),
"appID": (typeof type === 'number' || !isNaN(parseInt(type, 10)) ? type : ''), appID: (typeof type == 'number' || !isNaN(parseInt(type, 10)) ? type : ''),
"serverIP": server.ip, serverIP: server.ip,
"serverPassword": server.password, serverPassword: server.password,
"notes": description, notes: description,
"eventQuickTime": "now" 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.httpRequestPost({
"uri": "https://steamcommunity.com/gid/" + gid.toString() + "/eventEdit",
"form": form
}, function(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
if (typeof server === 'function') {
callback = server;
server = {"ip": "", "password": ""};
} else if (typeof server === 'string') {
server = {"ip": server, "password": ""};
} else if (typeof server !== 'object') {
server = {"ip": "", "password": ""};
}
var 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) { if (time === null) {
@@ -332,155 +283,214 @@ SteamCommunity.prototype.editGroupEvent = function (gid, id, name, type, descrip
form.timeChoice = 'specific'; form.timeChoice = 'specific';
} }
var self = this;
this.httpRequestPost({ this.httpRequestPost({
"uri": "https://steamcommunity.com/gid/" + gid.toString() + "/eventEdit", url: `https://steamcommunity.com/gid/${gid.toString()}/eventEdit`,
"form": form form
}, function(err, response, body) { }, (err, response, body) => {
if(!callback) { if (!callback) {
return; return;
} }
callback(err || null); callback(err || null);
}, "steamcommunity"); }, 'steamcommunity');
}; };
SteamCommunity.prototype.deleteGroupEvent = function(gid, id, callback) { SteamCommunity.prototype.editGroupEvent = function(gid, id, name, type, description, time, server, callback) {
if (typeof gid === 'string') { if (typeof gid === 'string') {
gid = new SteamID(gid); gid = new SteamID(gid);
} }
var form = { // 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
"sessionid": this.getSessionID(), // Passing a number for type will make it a game event for that appid
"action": "deleteEvent",
"eventID": id 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'
}; };
var self = this; 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({ this.httpRequestPost({
"uri": "https://steamcommunity.com/gid/" + gid.toString() + "/eventEdit", url: `https://steamcommunity.com/gid/${gid.toString()}/eventEdit`,
"form": form form
}, function(err, response, body) { }, (err, response, body) => {
if(!callback) { if (!callback) {
return; return;
} }
callback(err || null); callback(err || null);
}, "steamcommunity"); }, 'steamcommunity');
}; };
SteamCommunity.prototype.setGroupPlayerOfTheWeek = function(gid, steamID, callback) { SteamCommunity.prototype.deleteGroupEvent = function(gid, id, callback) {
if(typeof gid === 'string') { if (typeof gid == 'string') {
gid = new SteamID(gid); gid = new SteamID(gid);
} }
if(typeof steamID === 'string') { 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); steamID = new SteamID(steamID);
} }
var self = this;
this.httpRequestPost({ this.httpRequestPost({
"uri": "https://steamcommunity.com/gid/" + gid.getSteamID64() + "/potwEdit", url: `https://steamcommunity.com/gid/${gid.getSteamID64()}/potwEdit`,
"form": { form: {
"xml": 1, xml: 1,
"action": "potw", action: 'potw',
"memberId": steamID.getSteam3RenderedID(), memberId: steamID.getSteam3RenderedID(),
"sessionid": this.getSessionID() sessionid: this.getSessionID()
} }
}, function(err, response, body) { }, (err, response, body) => {
if(!callback) { if (!callback) {
return; return;
} }
if(err || response.statusCode != 200) { if (err || response.statusCode != 200) {
callback(err || new Error("HTTP error " + response.statusCode)); callback(err || new Error(`HTTP error ${response.statusCode}`));
return; return;
} }
xml2js.parseString(body, function(err, results) { XML2JS.parseString(body, (err, results) => {
if(err) { if (err) {
callback(err); callback(err);
return; return;
} }
if(results.response.results[0] == 'OK') { if (results.response.results[0] == 'OK') {
callback(null, new SteamID(results.response.oldPOTW[0]), new SteamID(results.response.newPOTW[0])); callback(null, new SteamID(results.response.oldPOTW[0]), new SteamID(results.response.newPOTW[0]));
} else { } else {
callback(new Error(results.response.results[0])); callback(new Error(results.response.results[0]));
} }
}); });
}, "steamcommunity"); }, 'steamcommunity');
}; };
SteamCommunity.prototype.kickGroupMember = function(gid, steamID, callback) { SteamCommunity.prototype.kickGroupMember = function(gid, steamID, callback) {
if(typeof gid === 'string') { if (typeof gid == 'string') {
gid = new SteamID(gid); gid = new SteamID(gid);
} }
if(typeof steamID === 'string') { if (typeof steamID == 'string') {
steamID = new SteamID(steamID); steamID = new SteamID(steamID);
} }
var self = this;
this.httpRequestPost({ this.httpRequestPost({
"uri": "https://steamcommunity.com/gid/" + gid.getSteamID64() + "/membersManage", url: `https://steamcommunity.com/gid/${gid.getSteamID64()}/membersManage`,
"form": { form: {
"sessionID": this.getSessionID(), sessionID: this.getSessionID(),
"action": "kick", action: 'kick',
"memberId": steamID.getSteamID64(), memberId: steamID.getSteamID64(),
"queryString": "" queryString: ''
} }
}, function(err, response, body) { }, (err, response, body) => {
if(!callback) { if (!callback) {
return; return;
} }
callback(err || null); callback(err || null);
}, "steamcommunity"); }, 'steamcommunity');
}; };
SteamCommunity.prototype.getGroupHistory = function(gid, page, callback) { SteamCommunity.prototype.getGroupHistory = function(gid, page, callback) {
if(typeof gid === 'string') { if (typeof gid == 'string') {
gid = new SteamID(gid); gid = new SteamID(gid);
} }
if(typeof page === 'function') { if (typeof page == 'function') {
callback = page; callback = page;
page = 1; page = 1;
} }
var self = this; this.httpRequest(`https://steamcommunity.com/gid/${gid.getSteamID64()}/history?p=${page}`, (err, response, body) => {
this.httpRequest("https://steamcommunity.com/gid/" + gid.getSteamID64() + "/history?p=" + page, function(err, response, body) {
if (err) { if (err) {
callback(err); callback(err);
return; return;
} }
var $ = Cheerio.load(body); let $ = Cheerio.load(body);
var output = {}; let output = {};
var paging = $('.group_paging p').text(); let paging = $('.group_paging p').text();
var match = paging.match(/(\d+) - (\d+) of (\d+)/); let match = paging.match(/(\d+) - (\d+) of (\d+)/);
if(match) { if (match) {
output.first = parseInt(match[1], 10); output.first = parseInt(match[1], 10);
output.last = parseInt(match[2], 10); output.last = parseInt(match[2], 10);
output.total = parseInt(match[3], 10); output.total = parseInt(match[3], 10);
} }
output.items = []; output.items = [];
var currentYear = (new Date()).getFullYear(); let currentYear = (new Date()).getFullYear();
var lastDate = Date.now(); let lastDate = Date.now();
Array.prototype.forEach.call($('.historyItem, .historyItemb'), function(item) { Array.prototype.forEach.call($('.historyItem, .historyItemb'), (item) => {
var data = {}; let data = {};
var $item = $(item); let $item = $(item);
data.type = $item.find('.historyShort').text().replace(/ /g, ''); data.type = $item.find('.historyShort').text().replace(/ /g, '');
var users = $item.find('.whiteLink[data-miniprofile]'); let users = $item.find('.whiteLink[data-miniprofile]');
var sid; let sid;
if(users[0]) { if (users[0]) {
sid = new SteamID(); sid = new SteamID();
sid.universe = SteamID.Universe.PUBLIC; sid.universe = SteamID.Universe.PUBLIC;
sid.type = SteamID.Type.INDIVIDUAL; sid.type = SteamID.Type.INDIVIDUAL;
@@ -489,7 +499,7 @@ SteamCommunity.prototype.getGroupHistory = function(gid, page, callback) {
data.user = sid; data.user = sid;
} }
if(users[1]) { if (users[1]) {
sid = new SteamID(); sid = new SteamID();
sid.universe = SteamID.Universe.PUBLIC; sid.universe = SteamID.Universe.PUBLIC;
sid.type = SteamID.Type.INDIVIDUAL; sid.type = SteamID.Type.INDIVIDUAL;
@@ -499,14 +509,14 @@ SteamCommunity.prototype.getGroupHistory = function(gid, page, callback) {
} }
// Figure out the date. Of course there's no year, because Valve // Figure out the date. Of course there's no year, because Valve
var dateParts = $item.find('.historyDate').text().split('@'); let dateParts = $item.find('.historyDate').text().split('@');
var date = dateParts[0].trim().replace(/(st|nd|th)$/, '').trim() + ', ' + currentYear; let date = dateParts[0].trim().replace(/(st|nd|th)$/, '').trim() + ', ' + currentYear;
var time = dateParts[1].trim().replace(/(am|pm)/, ' $1'); let time = dateParts[1].trim().replace(/(am|pm)/, ' $1');
date = new Date(date + ' ' + time + ' UTC'); 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 this date is in the future, or it's later than the previous one, decrement the year
if(date.getTime() > lastDate) { if (date.getTime() > lastDate) {
date.setFullYear(date.getFullYear() - 1); date.setFullYear(date.getFullYear() - 1);
} }
@@ -516,7 +526,99 @@ SteamCommunity.prototype.getGroupHistory = function(gid, page, callback) {
}); });
callback(null, output); callback(null, output);
}, "steamcommunity"); }, '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');
}; };
/** /**
@@ -525,30 +627,30 @@ SteamCommunity.prototype.getGroupHistory = function(gid, page, callback) {
* @param {function} callback - First argument is null/Error, second is array of SteamID objects * @param {function} callback - First argument is null/Error, second is array of SteamID objects
*/ */
SteamCommunity.prototype.getGroupJoinRequests = function(gid, callback) { SteamCommunity.prototype.getGroupJoinRequests = function(gid, callback) {
if (typeof gid === 'string') { if (typeof gid == 'string') {
gid = new SteamID(gid); gid = new SteamID(gid);
} }
this.httpRequestGet("https://steamcommunity.com/gid/" + gid.getSteamID64() + "/joinRequestsManage", (err, res, body) => { this.httpRequestGet(`https://steamcommunity.com/gid/${gid.getSteamID64()}/joinRequestsManage`, (err, res, body) => {
if (!body) { if (!body) {
callback(new Error("Malformed response")); callback(new Error('Malformed response'));
return; return;
} }
var matches = body.match(/JoinRequests_ApproveDenyUser\(\W*['"](\d+)['"],\W0\W\)/g); let matches = body.match(/JoinRequests_ApproveDenyUser\(\W*['"](\d+)['"],\W0\W\)/g);
if (!matches) { if (!matches) {
// no pending requests // no pending requests
callback(null, []); callback(null, []);
return; return;
} }
var requests = []; let requests = [];
for (var i = 0; i < matches.length; i++) { for (let i = 0; i < matches.length; i++) {
requests.push(new SteamID("[U:1:" + matches[i].match(/JoinRequests_ApproveDenyUser\(\W*['"](\d+)['"],\W0\W\)/)[1] + "]")); requests.push(new SteamID('[U:1:' + matches[i].match(/JoinRequests_ApproveDenyUser\(\W*['"](\d+)['"],\W0\W\)/)[1] + ']'));
} }
callback(null, requests); callback(null, requests);
}, "steamcommunity"); }, 'steamcommunity');
}; };
/** /**
@@ -559,34 +661,34 @@ SteamCommunity.prototype.getGroupJoinRequests = function(gid, callback) {
* @param {function} callback - Takes only an Error object/null as the first argument * @param {function} callback - Takes only an Error object/null as the first argument
*/ */
SteamCommunity.prototype.respondToGroupJoinRequests = function(gid, steamIDs, approve, callback) { SteamCommunity.prototype.respondToGroupJoinRequests = function(gid, steamIDs, approve, callback) {
if (typeof gid === 'string') { if (typeof gid == 'string') {
gid = new SteamID(gid); gid = new SteamID(gid);
} }
var rgAccounts = (!Array.isArray(steamIDs) ? [steamIDs] : steamIDs).map(sid => sid.toString()); let rgAccounts = (!Array.isArray(steamIDs) ? [steamIDs] : steamIDs).map(sid => sid.toString());
this.httpRequestPost({ this.httpRequestPost({
"uri": "https://steamcommunity.com/gid/" + gid.getSteamID64() + "/joinRequestsManage", url: `https://steamcommunity.com/gid/${gid.getSteamID64()}/joinRequestsManage`,
"form": { form: {
"rgAccounts": rgAccounts, rgAccounts: rgAccounts,
"bapprove": approve ? "1" : "0", bapprove: approve ? '1' : '0',
"json": "1", json: '1',
"sessionID": this.getSessionID() sessionID: this.getSessionID()
}, },
"json": true json: true
}, (err, res, body) => { }, (err, res, body) => {
if (!callback) { if (!callback) {
return; return;
} }
if (body != EResult.OK) { if (body != EResult.OK) {
var err = new Error(EResult[body] || ("Error " + body)); let err = new Error(EResult[body] || `Error ${body}`);
err.eresult = body; err.eresult = body;
callback(err); callback(err);
} else { } else {
callback(null); callback(null);
} }
}, "steamcommunity"); }, 'steamcommunity');
}; };
/** /**
@@ -596,30 +698,30 @@ SteamCommunity.prototype.respondToGroupJoinRequests = function(gid, steamIDs, ap
* @param {function} callback - Takes only an Error object/null as the first argument * @param {function} callback - Takes only an Error object/null as the first argument
*/ */
SteamCommunity.prototype.respondToAllGroupJoinRequests = function(gid, approve, callback) { SteamCommunity.prototype.respondToAllGroupJoinRequests = function(gid, approve, callback) {
if (typeof gid === 'string') { if (typeof gid == 'string') {
gid = new SteamID(gid); gid = new SteamID(gid);
} }
this.httpRequestPost({ this.httpRequestPost({
"uri": "https://steamcommunity.com/gid/" + gid.getSteamID64() + "/joinRequestsManage", url: `https://steamcommunity.com/gid/${gid.getSteamID64()}/joinRequestsManage`,
"form": { form: {
"bapprove": approve ? "1" : "0", bapprove: approve ? '1' : '0',
"json": "1", json: '1',
"action": "bulkrespond", action: 'bulkrespond',
"sessionID": this.getSessionID() sessionID: this.getSessionID()
}, },
"json": true json: true
}, (err, res, body) => { }, (err, res, body) => {
if (!callback) { if (!callback) {
return; return;
} }
if (body != EResult.OK) { if (body != EResult.OK) {
var err = new Error(EResult[body] || ("Error " + body)); let err = new Error(EResult[body] || `Error ${body}`);
err.eresult = body; err.eresult = body;
callback(err); callback(err);
} else { } else {
callback(null); callback(null);
} }
}, "steamcommunity"); }, 'steamcommunity');
}; };

66
components/help.js Normal file
View 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();
}

View File

@@ -1,23 +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) { exports.isSteamID = function(input) {
var keys = Object.keys(input); return ['universe', 'type', 'instance', 'accountid'].every(prop => typeof input[prop] == 'number' || typeof input[prop] == 'bigint');
if (keys.length != 4) {
return false;
}
// Make sure it has the keys we expect
keys = keys.filter(function(item) {
return ['universe', 'type', 'instance', 'accountid'].indexOf(item) != -1;
});
return keys.length == 4;
}; };
exports.decodeSteamTime = function(time) { exports.decodeSteamTime = function(time) {
var parts = time.split('@'); let date = new Date();
if (!parts[0].match(/,/)) {
// no year, assume current year if (time.includes('@')) {
parts[0] += ", " + (new Date()).getFullYear(); 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 new Date(parts.join('@').replace(/(am|pm)/, ' $1') + " UTC"); // add a space so JS can decode it 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'));
}; };

View File

@@ -1,132 +1,166 @@
var SteamCommunity = require('../index.js'); const {HttpResponse} = require('@doctormckay/stdlib/http'); // eslint-disable-line
const {betterPromise} = require('@doctormckay/stdlib/promises');
SteamCommunity.prototype.httpRequest = function(uri, options, callback, source) { const SteamCommunity = require('../index.js');
if (typeof uri === 'object') {
source = callback;
callback = options;
options = uri;
uri = options.url || options.uri;
} else if (typeof options === 'function') {
source = callback;
callback = options;
options = {};
}
options.url = options.uri = uri; /**
* @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 || '';
if (this._httpRequestConvenienceMethod) { await betterPromise((resolve, reject) => {
options.method = this._httpRequestConvenienceMethod; if (!this.onPreHttpRequest || !this.onPreHttpRequest(requestID, source, options, (err) => {
delete this._httpRequestConvenienceMethod; err ? reject(err) : resolve();
} })) {
// No pre-hook, or the pre-hook doesn't want to delay the request.
var requestID = ++this._httpRequestID; resolve();
source = source || "";
var self = this;
var continued = false;
if (!this.onPreHttpRequest || !this.onPreHttpRequest(requestID, source, options, continueRequest)) {
// No pre-hook, or the pre-hook doesn't want to delay the request.
continueRequest(null);
}
function continueRequest(err) {
if (continued) {
return;
}
continued = true;
if (err) {
if (callback) {
callback(err);
}
return;
}
self.request(options, function (err, response, body) {
var hasCallback = !!callback;
var httpError = options.checkHttpError !== false && self._checkHttpError(err, response, callback, body);
var communityError = !options.json && options.checkCommunityError !== false && self._checkCommunityError(body, httpError ? function () {} : callback); // don't fire the callback if hasHttpError did it already
var tradeError = !options.json && options.checkTradeError !== false && self._checkTradeError(body, httpError || communityError ? function () {} : callback); // don't fire the callback if either of the previous already did
var jsonError = options.json && options.checkJsonError !== false && !body ? new Error("Malformed JSON response") : null;
self.emit('postHttpRequest', requestID, source, options, httpError || communityError || tradeError || jsonError || null, response, body, {
"hasCallback": hasCallback,
"httpError": httpError,
"communityError": communityError,
"tradeError": tradeError,
"jsonError": jsonError
});
if (hasCallback && !(httpError || communityError || tradeError)) {
if (jsonError) {
callback.call(self, jsonError, response);
} else {
callback.apply(self, arguments);
}
} }
}); });
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();
} }
};
SteamCommunity.prototype.httpRequestGet = function() { let options = endpoint.endpoint ? endpoint : {};
this._httpRequestConvenienceMethod = "GET"; options.url = `https://steamcommunity.com${this._profileURL}/${endpoint.endpoint || endpoint}`;
return this.httpRequest.apply(this, arguments); options.followRedirect = true;
};
SteamCommunity.prototype.httpRequestPost = function() { if (form) {
this._httpRequestConvenienceMethod = "POST"; options.method = 'POST';
return this.httpRequest.apply(this, arguments); options.form = form;
} else if (!options.method) {
options.method = 'GET';
}
options.source = 'steamcommunity';
return await this.httpRequest(options);
}; };
SteamCommunity.prototype._notifySessionExpired = function(err) { SteamCommunity.prototype._notifySessionExpired = function(err) {
this.emit('sessionExpired', err); this.emit('sessionExpired', err);
}; };
SteamCommunity.prototype._checkHttpError = function(err, response, callback, body) { /**
if (err) { * @param {HttpResponse} response
callback(err, response, body); * @return {Error|boolean}
return err; * @private
} */
SteamCommunity.prototype._checkHttpError = function(response) {
if (response.statusCode >= 300 && response.statusCode <= 399 && response.headers.location.indexOf('/login') != -1) { if (response.statusCode >= 300 && response.statusCode <= 399 && response.headers.location.indexOf('/login') != -1) {
err = new Error("Not Logged In"); let err = new Error('Not Logged In');
callback(err, response, body);
this._notifySessionExpired(err); this._notifySessionExpired(err);
return err; return err;
} }
if (response.statusCode == 403 && typeof response.body === 'string' && response.body.match(/<div id="parental_notice_instructions">Enter your PIN below to exit Family View.<\/div>/)) { if (
err = new Error("Family View Restricted"); response.statusCode == 403
callback(err, response, body); && typeof response.textBody == 'string'
return err; && 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) { if (response.statusCode >= 400) {
err = new Error("HTTP error " + response.statusCode); let err = new Error(`HTTP error ${response.statusCode}`);
err.code = response.statusCode; err.code = response.statusCode;
callback(err, response, body);
return err; return err;
} }
return false; return false;
}; };
SteamCommunity.prototype._checkCommunityError = function(html, callback) { /**
var err; * @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>/)) { if (typeof html == 'string' && html.match(/<h1>Sorry!<\/h1>/)) {
var match = html.match(/<h3>(.+)<\/h3>/); let match = html.match(/<h3>(.+)<\/h3>/);
err = new Error(match ? match[1] : "Unknown error occurred"); return new Error(match ? match[1] : 'Unknown error occurred');
callback(err);
return err;
} }
if (typeof html === 'string' && html.match(/g_steamID = false;/) && html.match(/<h1>Sign In<\/h1>/)) { if (typeof html == 'string' && html.indexOf('g_steamID = false;') > -1 && html.indexOf('<title>Sign In</title>') > -1) {
err = new Error("Not Logged In"); let err = new Error('Not Logged In');
callback(err);
this._notifySessionExpired(err); this._notifySessionExpired(err);
return err; return err;
} }
@@ -134,16 +168,21 @@ SteamCommunity.prototype._checkCommunityError = function(html, callback) {
return false; return false;
}; };
SteamCommunity.prototype._checkTradeError = function(html, callback) { /**
* @param {HttpResponse} response
* @return {Error|boolean}
* @private
*/
SteamCommunity.prototype._checkTradeError = function(response) {
let html = response.textBody;
if (typeof html !== 'string') { if (typeof html !== 'string') {
return false; return false;
} }
var match = html.match(/<div id="error_msg">\s*([^<]+)\s*<\/div>/); let match = html.match(/<div id="error_msg">\s*([^<]+)\s*<\/div>/);
if (match) { if (match) {
var err = new Error(match[1].trim()); return new Error(match[1].trim());
callback(err);
return err;
} }
return false; return false;

View File

@@ -1,188 +0,0 @@
var SteamCommunity = require('../index.js');
var CEconItem = require('../classes/CEconItem.js');
var SteamID = require('steamid');
var request = require('request');
var Cheerio = require('cheerio');
var Async = require('async');
/*
* Inventory history in a nutshell.
*
* There are no more page numbers. Now you have to request after_time and optionally after_trade.
* Without "prev" set, you will request 30 trades that were completed FURTHER IN THE PAST than after_time (and optionally after_trade)
* With "prev" set, you will request 30 trades that were completed MORE RECENTLY than after_time (and optionally after_trade)
*/
/**
* @deprecated Use GetTradeHistory instead: https://lab.xpaw.me/steam_api_documentation.html#IEconService_GetTradeHistory_v1
* @param {object} options
* @param {function} callback
*/
SteamCommunity.prototype.getInventoryHistory = function(options, callback) {
if (typeof options === 'function') {
callback = options;
options = {};
}
options.direction = options.direction || "past";
var qs = "?l=english";
if (options.startTime) {
if (options.startTime instanceof Date) {
options.startTime = Math.floor(options.startTime.getTime() / 1000);
}
qs += "&after_time=" + options.startTime;
if (options.startTrade) {
qs += "&after_trade=" + options.startTrade;
}
}
if (options.direction == "future") {
qs += "&prev=1";
}
this._myProfile("inventoryhistory" + qs, null, function(err, response, body) {
if (err) {
callback(err);
return;
}
var output = {};
var vanityURLs = [];
var $ = Cheerio.load(body);
if (!$('.inventory_history_pagingrow').html()) {
callback(new Error("Malformed page: no paging row found"));
return;
}
// Load the inventory item data
var match2 = body.match(/var g_rgHistoryInventory = (.*);/);
if (!match2) {
callback(new Error("Malformed page: no trade found"));
return;
}
try {
var historyInventory = JSON.parse(match2[1]);
} catch (ex) {
callback(new Error("Malformed page: no well-formed trade data found"));
return;
}
var i;
// See if we've got paging buttons
var $paging = $('.inventory_history_nextbtn .pagebtn:not(.disabled)');
var href;
for (i = 0; i < $paging.length; i++) {
href = $paging[i].attribs.href;
if (href.match(/prev=1/)) {
output.firstTradeTime = new Date(href.match(/after_time=(\d+)/)[1] * 1000);
output.firstTradeID = href.match(/after_trade=(\d+)/)[1];
} else {
output.lastTradeTime = new Date(href.match(/after_time=(\d+)/)[1] * 1000);
output.lastTradeID = href.match(/after_trade=(\d+)/)[1];
}
}
output.trades = [];
var trades = $('.tradehistoryrow');
var item, trade, profileLink, items, j, econItem, timeMatch, time;
for (i = 0; i < trades.length; i++) {
item = $(trades[i]);
trade = {};
trade.onHold = !!item.find('span:nth-of-type(2)').text().match(/Trade on Hold/i);
timeMatch = item.find('.tradehistory_timestamp').html().match(/(\d+):(\d+)(am|pm)/);
if (timeMatch[1] == 12 && timeMatch[3] == 'am') {
timeMatch[1] = 0;
}
if (timeMatch[1] < 12 && timeMatch[3] == 'pm') {
timeMatch[1] = parseInt(timeMatch[1], 10) + 12;
}
time = (timeMatch[1] < 10 ? '0' : '') + timeMatch[1] + ':' + timeMatch[2] + ':00';
trade.date = new Date(item.find('.tradehistory_date').html() + ' ' + time + ' UTC');
trade.partnerName = item.find('.tradehistory_event_description a').html();
trade.partnerSteamID = null;
trade.partnerVanityURL = null;
trade.itemsReceived = [];
trade.itemsGiven = [];
profileLink = item.find('.tradehistory_event_description a').attr('href');
if (profileLink.indexOf('/profiles/') != -1) {
trade.partnerSteamID = new SteamID(profileLink.match(/(\d+)$/)[1]);
} else {
trade.partnerVanityURL = profileLink.match(/\/([^\/]+)$/)[1];
if (options.resolveVanityURLs && vanityURLs.indexOf(trade.partnerVanityURL) == -1) {
vanityURLs.push(trade.partnerVanityURL);
}
}
items = item.find('.history_item');
for (j = 0; j < items.length; j++) {
match = body.match(new RegExp("HistoryPageCreateItemHover\\( '" + $(items[j]).attr('id') + "', (\\d+), '(\\d+)', '(\\d+|class_\\d+_instance_\\d+|class_\\d+)', '(\\d+)' \\);"));
econItem = historyInventory[match[1]][match[2]][match[3]];
if ($(items[j]).attr('id').indexOf('received') != -1) {
trade.itemsReceived.push(new CEconItem(econItem));
} else {
trade.itemsGiven.push(new CEconItem(econItem));
}
}
output.trades.push(trade);
}
if (options.resolveVanityURLs) {
Async.map(vanityURLs, resolveVanityURL, function(err, results) {
if (err) {
callback(err);
return;
}
for (i = 0; i < output.trades.length; i++) {
if (output.trades[i].partnerSteamID || !output.trades[i].partnerVanityURL) {
continue;
}
// Find the vanity URL
for (j = 0; j < results.length; j++) {
if (results[j].vanityURL == output.trades[i].partnerVanityURL) {
output.trades[i].partnerSteamID = new SteamID(results[j].steamID);
break;
}
}
}
callback(null, output);
});
} else {
callback(null, output);
}
}, "steamcommunity");
};
function resolveVanityURL(vanityURL, callback) {
request("https://steamcommunity.com/id/" + vanityURL + "/?xml=1", function(err, response, body) {
if (err) {
callback(err);
return;
}
var match = body.match(/<steamID64>(\d+)<\/steamID64>/);
if (!match || !match[1]) {
callback(new Error("Couldn't find Steam ID"));
return;
}
callback(null, {"vanityURL": vanityURL, "steamID": match[1]});
});
}

View File

@@ -1,27 +1,376 @@
var SteamCommunity = require('../index.js'); const Cheerio = require('cheerio');
var 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) { SteamCommunity.prototype.getMarketApps = function(callback) {
var self = this; this.httpRequest('https://steamcommunity.com/market/', (err, response, body) => {
this.httpRequest('https://steamcommunity.com/market/', function (err, response, body) {
if (err) { if (err) {
callback(err); callback(err);
return; return;
} }
var $ = Cheerio.load(body); let $ = Cheerio.load(body);
if ($('.market_search_game_button_group')) { if ($('.market_search_game_button_group')) {
apps = {}; let apps = {};
$('.market_search_game_button_group > a').each(function (i, element) { $('.market_search_game_button_group a.game_button').each((i, element) => {
var e = Cheerio.load(element); let e = Cheerio.load(element);
var name = e('.game_button_game_name').text().trim(); let name = e('.game_button_game_name').text().trim();
var url = element.attribs.href; let url = element.attribs.href;
var appid = url.substr(url.indexOf('=') + 1); let appid = url.substr(url.indexOf('=') + 1);
apps[appid] = name; apps[appid] = name;
}); });
callback(null, apps); callback(null, apps);
} else { } else {
callback(new Error("Malformed response")); callback(new Error('Malformed response'));
} }
}, "steamcommunity"); }, '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);
}; };

View File

@@ -1,285 +1,284 @@
var SteamCommunity = require('../index.js'); const Cheerio = require('cheerio');
var SteamID = require('steamid'); const FS = require('fs');
var Cheerio = require('cheerio');
var fs = require('fs'); const Helpers = require('./helpers.js');
const SteamCommunity = require('../index.js');
SteamCommunity.PrivacyState = { SteamCommunity.PrivacyState = {
"Private": 1, Private: 1,
"FriendsOnly": 2, FriendsOnly: 2,
"Public": 3 Public: 3
}; };
var CommentPrivacyState = { const CommentPrivacyState = {
"1": "commentselfonly", 1: 2, // private
"2": "commentfriendsonly", 2: 0, // friends only
"3": "commentanyone" 3: 1 // anyone
}; };
/**
* Creates a profile page if you don't already have one.
* @param {function} callback
*/
SteamCommunity.prototype.setupProfile = function(callback) { SteamCommunity.prototype.setupProfile = function(callback) {
var self = this; this._myProfile('edit?welcomed=1', null, (err, response, body) => {
this._myProfile("edit?welcomed=1", null, function(err, response, body) { if (!callback) {
if(!callback) {
return; return;
} }
if(err || response.statusCode != 200) { if (err || response.statusCode != 200) {
callback(err || new Error("HTTP error " + response.statusCode)); callback(err || new Error('HTTP error ' + response.statusCode));
} else { } else {
callback(null); callback(null);
} }
}); });
}; };
/**
* Edits your profile details.
* @param {object} settings
* @param {function} callback
*/
SteamCommunity.prototype.editProfile = function(settings, callback) { SteamCommunity.prototype.editProfile = function(settings, callback) {
var self = this; this._myProfile('edit/info', null, (err, response, body) => {
this._myProfile("edit", null, function(err, response, body) { if (err || response.statusCode != 200) {
if(err || response.statusCode != 200) { if (callback) {
if(callback) { callback(err || new Error('HTTP error ' + response.statusCode));
callback(err || new Error("HTTP error " + response.statusCode));
} }
return; return;
} }
var $ = Cheerio.load(body); let $ = Cheerio.load(body);
var form = $('#editForm'); let existingSettings = $('#profile_edit_config').data('profile-edit');
if(!form) { if (!existingSettings || !existingSettings.strPersonaName) {
if(callback) { if (callback) {
callback(new Error("Malformed response")); callback(new Error('Malformed response'));
} }
return; return;
} }
var values = {}; let values = {
form.serializeArray().forEach(function(item) { sessionID: this.getSessionID(),
values[item.name] = item.value; type: 'profileSave',
}); weblink_1_title: '',
weblink_1_url: '',
for(var i in settings) { weblink_2_title: '',
if(!settings.hasOwnProperty(i)) { weblink_2_url: '',
continue; weblink_3_title: '',
} weblink_3_url: '',
personaName: existingSettings.strPersonaName,
switch(i) { 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': case 'name':
values.personaName = settings[i]; values.personaName = settings[i];
break; break;
case 'realName': case 'realName':
values.real_name = settings[i]; values.real_name = settings[i];
break; break;
case 'summary': case 'summary':
values.summary = settings[i]; values.summary = settings[i];
break; break;
case 'country': case 'country':
values.country = settings[i]; values.country = settings[i];
break; break;
case 'state': case 'state':
values.state = settings[i]; values.state = settings[i];
break; break;
case 'city': case 'city':
values.city = settings[i]; values.city = settings[i];
break; break;
case 'customURL': case 'customURL':
values.customURL = settings[i]; values.customURL = settings[i];
break; break;
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;
case 'primaryGroup': case 'primaryGroup':
if(typeof settings[i] === 'object' && settings[i].getSteamID64) { if(typeof settings[i] === 'object' && settings[i].getSteamID64) {
values.primary_group_steamid = settings[i].getSteamID64(); values.primary_group_steamid = settings[i].getSteamID64();
} else { } else {
values.primary_group_steamid = new SteamID(settings[i]).getSteamID64(); values.primary_group_steamid = new SteamID(settings[i]).getSteamID64();
} }
break; 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 // TODO: profile showcases
} }
} }
self._myProfile("edit", values, function(err, response, body) { this._myProfile('edit', values, (err, response, body) => {
if (settings.customURL) { if (settings.customURL) {
delete self._profileURL; delete this._profileURL;
} }
if(err || response.statusCode != 200) { if (!callback) {
if(callback) {
callback(err || new Error("HTTP error " + response.statusCode));
}
return; return;
} }
// Check for an error if (err || response.statusCode != 200) {
var $ = Cheerio.load(body); callback(err || new Error('HTTP error ' + response.statusCode));
var error = $('#errorText .formRowFields'); return;
if(error) {
error = error.text().trim();
if(error) {
if(callback) {
callback(new Error(error));
}
return;
}
} }
if(callback) { try {
let json = JSON.parse(body);
let err2 = Helpers.eresultError(json.success, json.errmsg);
if (err2) {
return callback(err2);
}
callback(null); callback(null);
} catch (ex) {
callback(ex);
} }
}); });
}); });
}; };
SteamCommunity.prototype.profileSettings = function(settings, callback) { SteamCommunity.prototype.profileSettings = function(settings, callback) {
var self = this; this._myProfile('edit/settings', null, (err, response, body) => {
this._myProfile("edit/settings", null, function(err, response, body) { if (err || response.statusCode != 200) {
if(err || response.statusCode != 200) { if (callback) {
if(callback) { callback(err || new Error('HTTP error ' + response.statusCode));
callback(err || new Error("HTTP error " + response.statusCode));
} }
return; return;
} }
var $ = Cheerio.load(body); let $ = Cheerio.load(body);
var form = $('#editForm'); let existingSettings = $('#profile_edit_config').data('profile-edit');
if(!form) { if (!existingSettings || !existingSettings.Privacy) {
if(callback) { if (callback) {
callback(new Error("Malformed response")); callback(new Error('Malformed response'));
} }
return; return;
} }
var values = {}; // PrivacySettings => {PrivacyProfile, PrivacyInventory, PrivacyInventoryGifts, PrivacyOwnedGames, PrivacyPlaytime}
form.serializeArray().forEach(function(item) { // eCommentPermission
values[item.name] = item.value; let privacy = existingSettings.Privacy.PrivacySettings;
}); let commentPermission = existingSettings.Privacy.eCommentPermission;
for(var i in settings) { for (let i in settings) {
if(!settings.hasOwnProperty(i)) { switch (i) {
continue;
}
switch(i) {
case 'profile': case 'profile':
values.privacySetting = settings[i]; privacy.PrivacyProfile = settings[i];
break; break;
case 'comments': case 'comments':
values.commentSetting = CommentPrivacyState[settings[i]]; commentPermission = CommentPrivacyState[settings[i]];
break; break;
case 'inventory': case 'inventory':
values.inventoryPrivacySetting = settings[i]; privacy.PrivacyInventory = settings[i];
break; break;
case 'inventoryGifts': case 'inventoryGifts':
values.inventoryGiftPrivacy = settings[i] ? 1 : 0; 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; break;
} }
} }
self._myProfile("edit/settings", values, function(err, response, body) { this._myProfile({
if(err || response.statusCode != 200) { method: 'POST',
if(callback) { endpoint: 'ajaxsetprivacy/',
callback(err || new Error("HTTP error " + response.statusCode)); 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; return;
} }
if(callback) { let err2 = Helpers.eresultError(body.success);
callback(null); if (err2) {
callback && callback(err2);
return;
}
if (callback) {
callback(null, body.Privacy);
} }
}); });
}); });
}; };
SteamCommunity.prototype.uploadAvatar = function(image, format, callback) { SteamCommunity.prototype.uploadAvatar = function(image, format, callback) {
if(typeof format === 'function') { if (typeof format === 'function') {
callback = format; callback = format;
format = null; format = null;
} }
var self = this; // are we logged in?
if (!this.steamID) {
if(image instanceof Buffer) { callback(new Error('Not Logged In'));
doUpload(image); return;
} else if(image.match(/^https?:\/\//)) {
this.httpRequestGet({
"uri": image,
"encoding": null
}, function(err, response, body) {
if(err || response.statusCode != 200) {
if(callback) {
callback(err ? new Error(err.message + " downloading image") : new Error("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, function(err, file) {
if(err) {
if(callback) {
callback(err);
}
return;
}
doUpload(file);
})
} }
function doUpload(buffer) { const doUpload = (buffer) => {
if(!format) { if (!format) {
if(callback) { if (callback) {
callback(new Error("Unknown image format")); callback(new Error('Unknown image format'));
} }
return; return;
} }
if(format.match(/^image\//)) { if (format.match(/^image\//)) {
format = format.substring(6); format = format.substring(6);
} }
var filename = ''; let filename = '';
var contentType = ''; let contentType = '';
switch(format.toLowerCase()) { switch (format.toLowerCase()) {
case 'jpg': case 'jpg':
case 'jpeg': case 'jpeg':
filename = 'avatar.jpg'; filename = 'avatar.jpg';
@@ -297,67 +296,160 @@ SteamCommunity.prototype.uploadAvatar = function(image, format, callback) {
break; break;
default: default:
if(callback) { if (callback) {
callback(new Error("Unknown or invalid image format")); callback(new Error('Unknown or invalid image format'));
} }
return; return;
} }
self.httpRequestPost({ this.httpRequestPost({
"uri": "https://steamcommunity.com/actions/FileUploader", url: 'https://steamcommunity.com/actions/FileUploader',
"formData": { formData: {
"MAX_FILE_SIZE": buffer.length, MAX_FILE_SIZE: buffer.length,
"type": "player_avatar_image", type: 'player_avatar_image',
"sId": self.steamID.getSteamID64(), sId: this.steamID.getSteamID64(),
"sessionid": self.getSessionID(), sessionid: this.getSessionID(),
"doSub": 1, doSub: 1,
"json": 1, json: 1,
"avatar": { avatar: {
"value": buffer, value: buffer,
"options": { options: {
"filename": filename, filename: filename,
"contentType": contentType contentType: contentType
} }
} }
}, },
"json": true json: true
}, function(err, response, body) { }, (err, response, body) => {
if(err) { if (err) {
if(callback) { 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); callback(err);
} }
return; return;
} }
if(body && !body.success && body.message) { doUpload(file);
if(callback) { });
callback(new Error(body.message));
}
return;
}
if(response.statusCode != 200) {
if(callback) {
callback(new Error("HTTP error " + response.statusCode));
}
return;
}
if(!body || !body.success) {
if(callback) {
callback(new Error("Malformed response"));
}
return;
}
if(callback) {
callback(null, body.images.full);
}
}, "steamcommunity");
} }
}; };
/**
* 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
View 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();
});
};

View File

@@ -1,159 +1,164 @@
var SteamTotp = require('steam-totp'); const StdLib = require('@doctormckay/stdlib');
var SteamCommunity = require('../index.js'); const SteamTotp = require('steam-totp');
var ETwoFactorTokenType = { const SteamCommunity = require('../index.js');
"None": 0, // No token-based two-factor authentication const Helpers = require('./helpers.js');
"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. 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) { SteamCommunity.prototype.enableTwoFactor = function(callback) {
var self = this; return StdLib.Promises.callbackPromise(null, callback, false, async (resolve, reject) => {
this._verifyMobileAccessToken();
this.getWebApiOauthToken(function(err, token) { if (!this.mobileAccessToken) {
if(err) { return reject(new Error('No mobile access token available. Provide one by calling setMobileAppAccessToken()'));
callback(err);
return;
} }
self.httpRequestPost({ let {jsonBody} = await this.httpRequest({
"uri": "https://api.steampowered.com/ITwoFactorService/AddAuthenticator/v1/", method: 'POST',
"form": { url: `https://api.steampowered.com/ITwoFactorService/AddAuthenticator/v1/?access_token=${this.mobileAccessToken}`,
"steamid": self.steamID.getSteamID64(), // TODO: Send this as protobuf to more closely mimic official app behavior
"access_token": token, form: {
"authenticator_time": Math.floor(Date.now() / 1000), steamid: this.steamID.getSteamID64(),
"authenticator_type": ETwoFactorTokenType.ValveMobileApp, authenticator_time: Math.floor(Date.now() / 1000),
"device_identifier": SteamTotp.getDeviceID(self.steamID), authenticator_type: ETwoFactorTokenType.ValveMobileApp,
"sms_phone_id": "1" device_identifier: SteamTotp.getDeviceID(this.steamID),
sms_phone_id: '1'
}, },
"json": true source: 'steamcommunity'
}, function(err, response, body) { });
if (err) {
callback(err);
return;
}
if(!body.response) {
callback(new Error("Malformed response"));
return;
}
if(body.response.status != 1) { if (!jsonBody.response) {
var error = new Error("Error " + body.response.status); return reject(new Error('Malformed response'));
error.eresult = body.response.status; }
callback(error);
return;
}
callback(null, body.response); if (jsonBody.response.status != 1) {
}, "steamcommunity"); 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) { SteamCommunity.prototype.finalizeTwoFactor = function(secret, activationCode, callback) {
var attemptsLeft = 30; return StdLib.Promises.callbackPromise(null, callback, false, async (resolve, reject) => {
var diff = 0; this._verifyMobileAccessToken();
var self = this; if (!this.mobileAccessToken) {
this.getWebApiOauthToken(function(err, token) { return reject(new Error('No mobile access token available. Provide one by calling setMobileAppAccessToken()'));
if(err) {
callback(err);
return;
} }
SteamTotp.getTimeOffset(function(err, offset, latency) { let attemptsLeft = 30;
if (err) { let diff = 0;
callback(err);
return;
}
diff = offset; await new Promise((resolve, reject) => {
finalize(token); SteamTotp.getTimeOffset(function(err, offset, latency) {
if (err) {
return reject(err);
}
diff = offset;
resolve();
});
}); });
});
function finalize(token) { let finalize = async () => {
var code = SteamTotp.generateAuthCode(secret, diff); let code = SteamTotp.generateAuthCode(secret, diff);
self.httpRequestPost({ let {jsonBody} = this.httpRequest({
"uri": "https://api.steampowered.com/ITwoFactorService/FinalizeAddAuthenticator/v1/", method: 'POST',
"form": { url: `https://api.steampowered.com/ITwoFactorService/FinalizeAddAuthenticator/v1/?access_token=${this.mobileAccessToken}`,
"steamid": self.steamID.getSteamID64(), form: {
"access_token": token, steamid: this.steamID.getSteamID64(),
"authenticator_code": code, authenticator_code: code,
"authenticator_time": Math.floor(Date.now() / 1000), authenticator_time: Math.floor(Date.now() / 1000),
"activation_code": activationCode activation_code: activationCode
}, },
"json": true source: 'steamcommunity'
}, function(err, response, body) { });
if (err) {
callback(err); if (!jsonBody.response) {
return; return reject(new Error('Malformed response'));
} }
if(!body.response) { jsonBody = jsonBody.response;
callback(new Error("Malformed response"));
return; if (jsonBody.server_time) {
diff = jsonBody.server_time - Math.floor(Date.now() / 1000);
} }
body = body.response; if (jsonBody.status == SteamCommunity.EResult.TwoFactorActivationCodeMismatch) {
return reject(new Error('Invalid activation code'));
if(body.server_time) { } else if (jsonBody.want_more) {
diff = body.server_time - Math.floor(Date.now() / 1000); if (--attemptsLeft <= 0) {
} // We made more than 30 attempts, something must be wrong
return reject(Helpers.eresultError(SteamCommunity.EResult.Fail));
if(body.status == 89) { }
callback(new Error("Invalid activation code"));
} else if(body.want_more) {
attemptsLeft--;
diff += 30; diff += 30;
finalize(token); finalize();
} else if(!body.success) { } else if (!jsonBody.success) {
callback(new Error("Error " + body.status)); return reject(new Error(`Error ${jsonBody.status}`));
} else { } else {
callback(null); resolve();
} }
}, "steamcommunity"); };
}
finalize();
});
}; };
/**
* @param {string} revocationCode
* @param {function} [callback]
* @return Promise<void>
*/
SteamCommunity.prototype.disableTwoFactor = function(revocationCode, callback) { SteamCommunity.prototype.disableTwoFactor = function(revocationCode, callback) {
var self = this; return StdLib.Promises.callbackPromise(null, callback, false, async (resolve, reject) => {
this._verifyMobileAccessToken();
this.getWebApiOauthToken(function(err, token) { if (!this.mobileAccessToken) {
if(err) { callback(new Error('No mobile access token available. Provide one by calling setMobileAppAccessToken()'));
callback(err);
return; return;
} }
self.httpRequestPost({ let {jsonBody} = await this.httpRequest({
"uri": "https://api.steampowered.com/ITwoFactorService/RemoveAuthenticator/v1/", method: 'POST',
"form": { url: `https://api.steampowered.com/ITwoFactorService/RemoveAuthenticator/v1/?access_token=${this.mobileAccessToken}`,
"steamid": self.steamID.getSteamID64(), form: {
"access_token": token, steamid: this.steamID.getSteamID64(),
"revocation_code": revocationCode, revocation_code: revocationCode,
"steamguard_scheme": 1 steamguard_scheme: 1
}, },
"json": true source: 'steamcommunity'
}, function(err, response, body) { });
if (err) {
callback(err);
return;
}
if(!body.response) { if (!jsonBody.response) {
callback(new Error("Malformed response")); return reject(new Error('Malformed response'));
return; }
}
if(!body.response.success) { if (!jsonBody.response.success) {
callback(new Error("Request failed")); return reject(new Error('Request failed'));
return; }
}
// success = true means it worked // success = true means it worked
callback(null); resolve();
}, "steamcommunity");
}); });
}; };

View File

@@ -1,119 +1,119 @@
var SteamCommunity = require('../index.js'); const Cheerio = require('cheerio');
var SteamID = require('steamid'); const Crypto = require('crypto');
var CEconItem = require('../classes/CEconItem.js'); const imageSize = require('image-size');
var Helpers = require('./helpers.js'); 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) { SteamCommunity.prototype.addFriend = function(userID, callback) {
if(typeof userID === 'string') { if (typeof userID === 'string') {
userID = new SteamID(userID); userID = new SteamID(userID);
} }
var self = this;
this.httpRequestPost({ this.httpRequestPost({
"uri": "https://steamcommunity.com/actions/AddFriendAjax", url: 'https://steamcommunity.com/actions/AddFriendAjax',
"form": { form: {
"accept_invite": 0, accept_invite: 0,
"sessionID": this.getSessionID(), sessionID: this.getSessionID(),
"steamid": userID.toString() steamid: userID.toString()
}, },
"json": true json: true
}, function(err, response, body) { }, (err, response, body) => {
if(!callback) { if (!callback) {
return; return;
} }
if (err) { if (err) {
callback(err); return callback(err);
return;
} }
if(body.success) { if (body.success) {
callback(null); callback(null);
} else { } else {
callback(new Error("Unknown error")); callback(new Error('Unknown error'));
} }
}, "steamcommunity"); }, 'steamcommunity');
}; };
SteamCommunity.prototype.acceptFriendRequest = function(userID, callback) { SteamCommunity.prototype.acceptFriendRequest = function(userID, callback) {
if(typeof userID === 'string') { if (typeof userID === 'string') {
userID = new SteamID(userID); userID = new SteamID(userID);
} }
var self = this;
this.httpRequestPost({ this.httpRequestPost({
"uri": "https://steamcommunity.com/actions/AddFriendAjax", url: 'https://steamcommunity.com/actions/AddFriendAjax',
"form": { form: {
"accept_invite": 1, accept_invite: 1,
"sessionID": this.getSessionID(), sessionID: this.getSessionID(),
"steamid": userID.toString() steamid: userID.toString()
} }
}, function(err, response, body) { }, (err, response, body) => {
if(!callback) { if (!callback) {
return; return;
} }
callback(err || null); callback(err || null);
}, "steamcommunity"); }, 'steamcommunity');
}; };
SteamCommunity.prototype.removeFriend = function(userID, callback) { SteamCommunity.prototype.removeFriend = function(userID, callback) {
if(typeof userID === 'string') { if (typeof userID === 'string') {
userID = new SteamID(userID); userID = new SteamID(userID);
} }
var self = this;
this.httpRequestPost({ this.httpRequestPost({
"uri": "https://steamcommunity.com/actions/RemoveFriendAjax", url: 'https://steamcommunity.com/actions/RemoveFriendAjax',
"form": { form: {
"sessionID": this.getSessionID(), sessionID: this.getSessionID(),
"steamid": userID.toString() steamid: userID.toString()
} }
}, function(err, response, body) { }, (err, response, body) => {
if(!callback) { if (!callback) {
return; return;
} }
callback(err || null); callback(err || null);
}, "steamcommunity"); }, 'steamcommunity');
}; };
SteamCommunity.prototype.blockCommunication = function(userID, callback) { SteamCommunity.prototype.blockCommunication = function(userID, callback) {
if(typeof userID === 'string') { if (typeof userID === 'string') {
userID = new SteamID(userID); userID = new SteamID(userID);
} }
var self = this;
this.httpRequestPost({ this.httpRequestPost({
"uri": "https://steamcommunity.com/actions/BlockUserAjax", url: 'https://steamcommunity.com/actions/BlockUserAjax',
"form": { form: {
"sessionID": this.getSessionID(), sessionID: this.getSessionID(),
"steamid": userID.toString() steamid: userID.toString()
} }
}, function(err, response, body) { }, (err, response, body) => {
if(!callback) { if (!callback) {
return; return;
} }
callback(err || null); callback(err || null);
}, "steamcommunity"); }, 'steamcommunity');
}; };
SteamCommunity.prototype.unblockCommunication = function(userID, callback) { SteamCommunity.prototype.unblockCommunication = function(userID, callback) {
if(typeof userID === 'string') { if (typeof userID === 'string') {
userID = new SteamID(userID); userID = new SteamID(userID);
} }
var form = {"action": "unignore"}; let form = {action: 'unignore'};
form['friends[' + userID.toString() + ']'] = 1; form['friends[' + userID.toString() + ']'] = 1;
this._myProfile('friends/blocked/', form, function(err, response, body) { this._myProfile('friends/blocked/', form, (err, response, body) => {
if(!callback) { if (!callback) {
return; return;
} }
if(err || response.statusCode >= 400) { if (err || response.statusCode >= 400) {
callback(err || new Error("HTTP error " + response.statusCode)); callback(err || new Error(`HTTP error ${response.statusCode}`));
return; return;
} }
@@ -122,21 +122,20 @@ SteamCommunity.prototype.unblockCommunication = function(userID, callback) {
}; };
SteamCommunity.prototype.postUserComment = function(userID, message, callback) { SteamCommunity.prototype.postUserComment = function(userID, message, callback) {
if(typeof userID === 'string') { if (typeof userID === 'string') {
userID = new SteamID(userID); userID = new SteamID(userID);
} }
var self = this;
this.httpRequestPost({ this.httpRequestPost({
"uri": "https://steamcommunity.com/comment/Profile/post/" + userID.toString() + "/-1", url: `https://steamcommunity.com/comment/Profile/post/${userID.toString()}/-1`,
"form": { form: {
"comment": message, comment: message,
"count": 6, count: 1,
"sessionid": this.getSessionID() sessionid: this.getSessionID()
}, },
"json": true json: true
}, function(err, response, body) { }, (err, response, body) => {
if(!callback) { if (!callback) {
return; return;
} }
@@ -145,34 +144,132 @@ SteamCommunity.prototype.postUserComment = function(userID, message, callback) {
return; return;
} }
if(body.success) { if (body.success) {
callback(null); const $ = Cheerio.load(body.comments_html);
} else if(body.error) { const commentID = $('.commentthread_comment').attr('id').split('_')[1];
callback(null, commentID);
} else if (body.error) {
callback(new Error(body.error)); callback(new Error(body.error));
} else { } else {
callback(new Error("Unknown error")); callback(new Error('Unknown error'));
} }
}, "steamcommunity"); }, '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) { SteamCommunity.prototype.inviteUserToGroup = function(userID, groupID, callback) {
if(typeof userID === 'string') { if (typeof userID === 'string') {
userID = new SteamID(userID); userID = new SteamID(userID);
} }
var self = this;
this.httpRequestPost({ this.httpRequestPost({
"uri": "https://steamcommunity.com/actions/GroupInvite", url: 'https://steamcommunity.com/actions/GroupInvite',
"form": { form: {
"group": groupID.toString(), group: groupID.toString(),
"invitee": userID.toString(), invitee: userID.toString(),
"json": 1, json: 1,
"sessionID": this.getSessionID(), sessionID: this.getSessionID(),
"type": "groupInvite" type: 'groupInvite'
}, },
"json": true json: true
}, function(err, response, body) { }, (err, response, body) => {
if(!callback) { if (!callback) {
return; return;
} }
@@ -181,14 +278,14 @@ SteamCommunity.prototype.inviteUserToGroup = function(userID, groupID, callback)
return; return;
} }
if(body.results == 'OK') { if (body.results == 'OK') {
callback(null); callback(null);
} else if(body.results) { } else if (body.results) {
callback(new Error(body.results)); callback(new Error(body.results));
} else { } else {
callback(new Error("Unknown error")); callback(new Error('Unknown error'));
} }
}, "steamcommunity"); }, 'steamcommunity');
}; };
SteamCommunity.prototype.getUserAliases = function(userID, callback) { SteamCommunity.prototype.getUserAliases = function(userID, callback) {
@@ -197,24 +294,63 @@ SteamCommunity.prototype.getUserAliases = function(userID, callback) {
} }
this.httpRequestGet({ this.httpRequestGet({
"uri": "https://steamcommunity.com/profiles/" + userID.getSteamID64() + "/ajaxaliases", url: `https://steamcommunity.com/profiles/${userID.getSteamID64()}/ajaxaliases`,
"json": true json: true
}, function(err, response, body) { }, (err, response, body) => {
if (err) { if (err) {
callback(err); callback(err);
return; return;
} }
if (typeof body !== 'object') { if (typeof body !== 'object') {
callback(new Error("Malformed response")); callback(new Error('Malformed response'));
return; return;
} }
callback(null, body.map(function(entry) { callback(null, body.map((entry) => {
entry.timechanged = Helpers.decodeSteamTime(entry.timechanged); entry.timechanged = Helpers.decodeSteamTime(entry.timechanged);
return entry; return entry;
})); }));
}, "steamcommunity"); }, '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) { SteamCommunity.prototype.getUserInventoryContexts = function(userID, callback) {
@@ -228,44 +364,47 @@ SteamCommunity.prototype.getUserInventoryContexts = function(userID, callback) {
} }
if (!userID) { if (!userID) {
callback(new Error("No SteamID specified and not logged in")); callback(new Error('No SteamID specified and not logged in'));
return; return;
} }
var self = this; this.httpRequest(`https://steamcommunity.com/profiles/${userID.getSteamID64()}/inventory/`, (err, response, body) => {
this.httpRequest("https://steamcommunity.com/profiles/" + userID.getSteamID64() + "/inventory/", function(err, response, body) {
if (err) { if (err) {
callback(err); callback(err);
return; return;
} }
var match = body.match(/var g_rgAppContextData = ([^\n]+);\r?\n/); let match = body.match(/var g_rgAppContextData = ([^\n]+);\r?\n/);
if (!match) { if (!match) {
var errorMessage = "Malformed response"; callback(new Error('Malformed response'));
if(body.match(/0 items in their inventory\./)){
callback(null, {});
return;
}else if(body.match(/inventory is currently private\./)){
errorMessage = "Private inventory";
}else if(body.match(/profile\_private\_info/)){
errorMessage = "Private profile";
}
callback(new Error(errorMessage));
return; return;
} }
var data; let data;
try { try {
data = JSON.parse(match[1]); data = JSON.parse(match[1]);
} catch(e) { } catch (e) {
callback(new Error("Malformed response")); callback(new Error('Malformed response'));
return; 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); callback(null, data);
}, "steamcommunity"); }, 'steamcommunity');
}; };
/** /**
@@ -278,27 +417,22 @@ SteamCommunity.prototype.getUserInventoryContexts = function(userID, callback) {
* @param {function} callback * @param {function} callback
*/ */
SteamCommunity.prototype.getUserInventory = function(userID, appID, contextID, tradableOnly, callback) { SteamCommunity.prototype.getUserInventory = function(userID, appID, contextID, tradableOnly, callback) {
var self = this;
if (typeof userID === 'string') { if (typeof userID === 'string') {
userID = new SteamID(userID); userID = new SteamID(userID);
} }
var endpoint = "/profiles/" + userID.getSteamID64(); const get = (inventory, currency, start) => {
get([], []); this.httpRequest({
url: `https://steamcommunity.com${endpoint}/inventory/json/${appID}/${contextID}`,
function get(inventory, currency, start) { headers: {
self.httpRequest({ Referer: `https://steamcommunity.com${endpoint}/inventory`
"uri": "https://steamcommunity.com" + endpoint + "/inventory/json/" + appID + "/" + contextID,
"headers": {
"Referer": "https://steamcommunity.com" + endpoint + "/inventory"
}, },
"qs": { qs: {
"start": start, start: start,
"trading": tradableOnly ? 1 : undefined trading: tradableOnly ? 1 : undefined
}, },
"json": true json: true
}, function(err, response, body) { }, (err, response, body) => {
if (err) { if (err) {
callback(err); callback(err);
return; return;
@@ -306,43 +440,37 @@ SteamCommunity.prototype.getUserInventory = function(userID, appID, contextID, t
if (!body || !body.success || !body.rgInventory || !body.rgDescriptions || !body.rgCurrency) { if (!body || !body.success || !body.rgInventory || !body.rgDescriptions || !body.rgCurrency) {
if (body) { if (body) {
callback(new Error(body.Error || "Malformed response")); callback(new Error(body.Error || 'Malformed response'));
} else { } else {
callback(new Error("Malformed response")); callback(new Error('Malformed response'));
} }
return; return;
} }
var i; for (let i in body.rgInventory) {
for (i in body.rgInventory) {
if (!body.rgInventory.hasOwnProperty(i)) {
continue;
}
inventory.push(new CEconItem(body.rgInventory[i], body.rgDescriptions, contextID)); inventory.push(new CEconItem(body.rgInventory[i], body.rgDescriptions, contextID));
} }
for (i in body.rgCurrency) { for (let i in body.rgCurrency) {
if (!body.rgCurrency.hasOwnProperty(i)) { currency.push(new CEconItem(body.rgCurrency[i], body.rgDescriptions, contextID));
continue;
}
currency.push(new CEconItem(body.rgInventory[i], body.rgDescriptions, contextID));
} }
if (body.more) { if (body.more) {
var match = response.request.uri.href.match(/\/(profiles|id)\/([^\/]+)\//); let match = response.request.uri.href.match(/\/(profiles|id)\/([^/]+)\//);
if(match) { if (match) {
endpoint = "/" + match[1] + "/" + match[2]; endpoint = `/${match[1]}/${match[2]}`;
} }
get(inventory, currency, body.more_start); get(inventory, currency, body.more_start);
} else { } else {
callback(null, inventory, currency); callback(null, inventory, currency);
} }
}, "steamcommunity"); }, 'steamcommunity');
} };
let endpoint = `/profiles/${userID.getSteamID64()}`;
get([], []);
}; };
/** /**
@@ -355,49 +483,66 @@ SteamCommunity.prototype.getUserInventory = function(userID, appID, contextID, t
* @param {function} callback * @param {function} callback
*/ */
SteamCommunity.prototype.getUserInventoryContents = function(userID, appID, contextID, tradableOnly, language, callback) { SteamCommunity.prototype.getUserInventoryContents = function(userID, appID, contextID, tradableOnly, language, callback) {
var self = this; 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') { if (typeof userID === 'string') {
userID = new SteamID(userID); userID = new SteamID(userID);
} }
if (typeof language === 'function') { // A bit of optimization; objects are hash tables so it's more efficient to look up by key than to iterate an array
callback = language; let quickDescriptionLookup = {};
language = "english";
}
var pos = 1; const getDescription = (descriptions, classID, instanceID) => {
get([], []); let key = classID + '_' + (instanceID || '0'); // instanceID can be undefined, in which case it's 0.
function get(inventory, currency, start) { if (quickDescriptionLookup[key]) {
self.httpRequest({ return quickDescriptionLookup[key];
"uri": "https://steamcommunity.com/inventory/" + userID.getSteamID64() + "/" + appID + "/" + contextID, }
"headers": {
"Referer": "https://steamcommunity.com/profiles/" + userID.getSteamID64() + "/inventory" 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": { qs: {
"l": language, // Default language l: language, // Default language
"count": 5000, // Max items per 'page' count: 2000, // Max items per 'page'
"start_assetid": start start_assetid: start
}, },
"json": true json: true
}, function(err, response, body) { }, (err, response, body) => {
if (err) { if (err) {
if (err.message == "HTTP error 403" && body === null) { if (err.message == 'HTTP error 403' && body === null) {
// 403 with a body of "null" means the inventory/profile is private. // 403 with a body of "null" means the inventory/profile is private.
if (self.steamID && userID.getSteamID64() == self.steamID.getSteamID64()) { if (this.steamID && userID.getSteamID64() == this.steamID.getSteamID64()) {
// We can never get private profile error for our own inventory! // We can never get private profile error for our own inventory!
self._notifySessionExpired(err); this._notifySessionExpired(err);
} }
callback(new Error("This profile is private.")); callback(new Error('This profile is private.'));
return; return;
} }
if (err.message == "HTTP error 500" && body && body.error) { if (err.message == 'HTTP error 500' && body && body.error) {
err = new Error(body.error); err = new Error(body.error);
var match = body.error.match(/^(.+) \((\d+)\)$/); let match = body.error.match(/^(.+) \((\d+)\)$/);
if (match) { if (match) {
err.message = match[1]; err.message = match[1];
err.eresult = match[2]; err.eresult = match[2];
@@ -419,17 +564,17 @@ SteamCommunity.prototype.getUserInventoryContents = function(userID, appID, cont
if (!body || !body.success || !body.assets || !body.descriptions) { if (!body || !body.success || !body.assets || !body.descriptions) {
if (body) { if (body) {
// Dunno if the error/Error property even exists on this new endpoint // Dunno if the error/Error property even exists on this new endpoint
callback(new Error(body.error || body.Error || "Malformed response")); callback(new Error(body.error || body.Error || 'Malformed response'));
} else { } else {
callback(new Error("Malformed response")); callback(new Error('Malformed response'));
} }
return; return;
} }
for (var i = 0; i < body.assets.length; i++) { for (let i = 0; i < body.assets.length; i++) {
var description = getDescription(body.descriptions, body.assets[i].classid, body.assets[i].instanceid); let description = getDescription(body.descriptions, body.assets[i].classid, body.assets[i].instanceid);
if (!tradableOnly || (description && description.tradable)) { if (!tradableOnly || (description && description.tradable)) {
body.assets[i].pos = pos++; body.assets[i].pos = pos++;
(body.assets[i].currencyid ? currency : inventory).push(new CEconItem(body.assets[i], description, contextID)); (body.assets[i].currencyid ? currency : inventory).push(new CEconItem(body.assets[i], description, contextID));
@@ -441,23 +586,164 @@ SteamCommunity.prototype.getUserInventoryContents = function(userID, appID, cont
} else { } else {
callback(null, inventory, currency, body.total_inventory_count); callback(null, inventory, currency, body.total_inventory_count);
} }
}, "steamcommunity"); }, 'steamcommunity');
} };
// A bit of optimization; objects are hash tables so it's more efficient to look up by key than to iterate an array let pos = 1;
var quickDescriptionLookup = {}; get([], []);
};
function getDescription(descriptions, classID, instanceID) {
var key = classID + '_' + (instanceID || '0'); // instanceID can be undefined, in which case it's 0. /**
* Upload an image to Steam and send it to another user over Steam chat.
if (quickDescriptionLookup[key]) { * @param {SteamID|string} userID - Either a SteamID object or a string that can parse into one
return quickDescriptionLookup[key]; * @param {Buffer} imageContentsBuffer - The image contents, as a Buffer
} * @param {{spoiler?: boolean}} [options]
* @param {function} callback
for (var i = 0; i < descriptions.length; i++) { */
quickDescriptionLookup[descriptions[i].classid + '_' + (descriptions[i].instanceid || '0')] = descriptions[i]; SteamCommunity.prototype.sendImageToUser = function(userID, imageContentsBuffer, options, callback) {
} if (typeof options == 'function') {
callback = options;
return quickDescriptionLookup[key]; 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');
}; };

View File

@@ -1,65 +1,109 @@
var SteamCommunity = require('../index.js'); 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) { SteamCommunity.prototype.getWebApiKey = function(domain, callback) {
var self = this; return StdLib.Promises.callbackPromise(['key'], callback, false, async (resolve, reject) => {
this.httpRequest({ let {textBody} = await this.httpRequest({
"uri": "https://steamcommunity.com/dev/apikey?l=english", method: 'GET',
"followRedirect": false url: 'https://steamcommunity.com/dev/apikey?l=english',
}, function(err, response, body) { followRedirect: false,
if (err) { source: 'steamcommunity'
callback(err); });
return;
if (textBody.includes('<h2>Access Denied</h2>')) {
return reject(new Error('Access Denied'));
} }
if(body.match(/<h2>Access Denied<\/h2>/)) { if (textBody.includes('You must have a validated email address to create a Steam Web API key.')) {
return callback(new Error("Access Denied")); return reject(new Error('You must have a validated email address to create a Steam Web API key.'));
} }
var match = body.match(/<p>Key: ([0-9A-F]+)<\/p>/); let match = textBody.match(/<p>Key: ([0-9A-F]+)<\/p>/);
if(match) { if (match) {
// We already have an API key registered // We already have an API key registered
callback(null, match[1]); return resolve({key: match[1]});
} else {
// We need to register a new API key
self.httpRequestPost('https://steamcommunity.com/dev/registerkey?l=english', {
"form": {
"domain": domain,
"agreeToTerms": "agreed",
"sessionid": self.getSessionID(),
"Submit": "Register"
}
}, function(err, response, body) {
if (err) {
callback(err);
return;
}
self.getWebApiKey(domain, callback);
}, "steamcommunity");
} }
}, "steamcommunity");
// 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)});
});
}; };
SteamCommunity.prototype.getWebApiOauthToken = function(callback) { /**
var self = this; * Sets an access_token generated by steam-session using EAuthTokenPlatformType.MobileApp.
* Required for some operations such as 2FA enabling and disabling.
if( this.oAuthToken ) { * This will throw an Error if the provided token is not valid, was not generated for the MobileApp platform, is expired,
return callback( null, this.oAuthToken ); * 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');
} }
// Pull an oauth token from the webchat UI let decodedToken = Helpers.decodeJwt(token);
this.httpRequest("https://steamcommunity.com/chat", function(err, response, body) {
if (err) {
callback(err);
return;
}
var match = body.match(/"([0-9a-f]{32})"/); if (!decodedToken.iss || !decodedToken.sub || !decodedToken.aud || !decodedToken.exp) {
if (!match) { throw new Error('Provided value is not a valid Steam access token');
callback(new Error("Malformed response")); }
return;
}
callback(null, match[1]); if (decodedToken.iss == 'steam') {
}, "steamcommunity"); 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
View 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
```

View 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');
}

View File

@@ -1,61 +1,135 @@
var SteamCommunity = require('../index.js'); // If you aren't running this script inside of the repository, replace the following line with:
var ReadLine = require('readline'); // const SteamCommunity = require('steamcommunity');
var fs = require('fs'); const SteamCommunity = require('../index.js');
const SteamSession = require('steam-session');
const ReadLine = require('readline');
var community = new SteamCommunity(); let g_AbortPromptFunc = null;
var rl = ReadLine.createInterface({
"input": process.stdin,
"output": process.stdout
});
rl.question("Username: ", function(accountName) { let community = new SteamCommunity();
rl.question("Password: ", function(password) {
rl.question("Two-Factor Auth Code: ", function(authCode) { main();
rl.question("Revocation Code: R", function(rCode) { async function main() {
doLogin(accountName, password, authCode, "", rCode); 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();
}); });
});
function doLogin(accountName, password, authCode, captcha, rCode) { session.on('timeout', () => {
community.login({ abortPrompt();
"accountName": accountName, console.log('This login attempt has timed out.');
"password": password, });
"twoFactorCode": authCode,
"captcha": captcha session.on('error', (err) => {
}, function(err, sessionID, cookies, steamguard) { abortPrompt();
if(err) {
if(err.message == 'SteamGuard') { // This should ordinarily not happen. This only happens in case there's some kind of unexpected error while
console.log("This account does not have two-factor authentication enabled."); // polling, e.g. the network connection goes down or Steam chokes on something.
process.exit();
return; 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.');
} }
if(err.message == 'CAPTCHA') { let code = await promptAsync('Code: ');
console.log(err.captchaurl); if (code) {
rl.question("CAPTCHA: ", function(captchaInput) { await session.submitSteamGuardCode(code);
doLogin(accountName, password, authCode, captchaInput);
});
return;
} }
// 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); console.log(err);
process.exit(); process.exit();
return; return;
} }
console.log("Logged on!"); console.log('Two-factor authentication disabled!');
community.disableTwoFactor("R" + rCode, function(err) { process.exit();
if(err) { });
console.log(err); }
process.exit();
return; // 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');
} }
console.log("Two-factor authentication disabled!"); g_AbortPromptFunc = null;
process.exit(); rl.close();
resolve(result);
}); });
}); });
} }
function abortPrompt() {
if (!g_AbortPromptFunc) {
return;
}
g_AbortPromptFunc();
process.stdout.write('\n');
}

View File

@@ -1,48 +1,47 @@
var SteamCommunity = require('../index.js'); let SteamCommunity = require('../index.js');
var ReadLine = require('readline'); let ReadLine = require('readline');
var fs = require('fs');
var community = new SteamCommunity(); let community = new SteamCommunity();
var rl = ReadLine.createInterface({ let rl = ReadLine.createInterface({
"input": process.stdin, input: process.stdin,
"output": process.stdout output: process.stdout
}); });
rl.question("Username: ", function(accountName) { rl.question('Username: ', function(accountName) {
rl.question("Password: ", function(password) { rl.question('Password: ', function(password) {
doLogin(accountName, password); doLogin(accountName, password);
}); });
}); });
function doLogin(accountName, password, authCode, twoFactorCode, captcha) { function doLogin(accountName, password, authCode, twoFactorCode, captcha) {
community.login({ community.login({
"accountName": accountName, accountName: accountName,
"password": password, password: password,
"authCode": authCode, authCode: authCode,
"twoFactorCode": twoFactorCode, twoFactorCode: twoFactorCode,
"captcha": captcha captcha: captcha
}, function(err, sessionID, cookies, steamguard) { }, function(err, sessionID, cookies, steamguard) {
if(err) { if (err) {
if(err.message == 'SteamGuardMobile') { if (err.message == 'SteamGuardMobile') {
rl.question("Steam Authenticator Code: ", function(code) { rl.question('Steam Authenticator Code: ', function(code) {
doLogin(accountName, password, null, code); doLogin(accountName, password, null, code);
}); });
return; return;
} }
if(err.message == 'SteamGuard') { if (err.message == 'SteamGuard') {
console.log("An email has been sent to your address at " + err.emaildomain); console.log('An email has been sent to your address at ' + err.emaildomain);
rl.question("Steam Guard Code: ", function(code) { rl.question('Steam Guard Code: ', function(code) {
doLogin(accountName, password, code); doLogin(accountName, password, code);
}); });
return; return;
} }
if(err.message == 'CAPTCHA') { if (err.message == 'CAPTCHA') {
console.log(err.captchaurl); console.log(err.captchaurl);
rl.question("CAPTCHA: ", function(captchaInput) { rl.question('CAPTCHA: ', function(captchaInput) {
doLogin(accountName, password, authCode, twoFactorCode, captchaInput); doLogin(accountName, password, authCode, twoFactorCode, captchaInput);
}); });
@@ -54,9 +53,9 @@ function doLogin(accountName, password, authCode, twoFactorCode, captcha) {
return; return;
} }
console.log("Logged on!"); console.log('Logged on!');
rl.question("Group ID: ", function(gid) { rl.question('Group ID: ', function(gid) {
community.getSteamGroup(gid, function(err, group) { community.getSteamGroup(gid, function(err, group) {
if (err) { if (err) {
console.log(err); console.log(err);
@@ -65,19 +64,19 @@ function doLogin(accountName, password, authCode, twoFactorCode, captcha) {
group.getAllAnnouncements(function(err, announcements) { group.getAllAnnouncements(function(err, announcements) {
if(announcements.length === 0) { if (announcements.length === 0) {
return console.log("This group has no announcements"); return console.log('This group has no announcements');
} }
for (var i = announcements.length - 1; i >= 0; i--) { 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); 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('Would you like to delete delete or edit an annoucement? (Type edit/delete): ', function(choice) {
rl.question("Annoucement ID: ", function(aid) { rl.question('Annoucement ID: ', function(aid) {
if(choice === 'edit') { if (choice === 'edit') {
rl.question("New title: ", function(header) { rl.question('New title: ', function(header) {
rl.question("New body: ", function(content) { rl.question('New body: ', function(content) {
// EW THE PYRAMID! // EW THE PYRAMID!
// Try replace this with delete! // Try replace this with delete!
editAnnouncement(group, aid, header, content); editAnnouncement(group, aid, header, content);
@@ -97,10 +96,10 @@ function doLogin(accountName, password, authCode, twoFactorCode, captcha) {
function editAnnouncement(group, aid, header, content) { function editAnnouncement(group, aid, header, content) {
// Actual community method. // Actual community method.
group.editAnnouncement(aid, header, content, function(error) { group.editAnnouncement(aid, header, content, function(error) {
if(!error) { if (!error) {
console.log("Annoucement edited!"); console.log('Annoucement edited!');
} else { } else {
console.log("Unable to edit annoucement! %j", error); console.log('Unable to edit annoucement! %j', error);
process.exit(1); process.exit(1);
} }
}); });
@@ -110,10 +109,10 @@ function deleteAnnouncement(group, aid) {
// group.deleteAnnouncement(aid); // group.deleteAnnouncement(aid);
// Or // Or
group.deleteAnnouncement(aid, function(err) { group.deleteAnnouncement(aid, function(err) {
if(!err) { if (!err) {
console.log("Deleted"); console.log('Deleted');
} else { } else {
console.log("Error deleting announcement."); console.log('Error deleting announcement.');
} }
}) });
} }

View File

@@ -1,48 +1,98 @@
var SteamCommunity = require('../index.js'); // If you aren't running this script inside of the repository, replace the following line with:
var ReadLine = require('readline'); // const SteamCommunity = require('steamcommunity');
var fs = require('fs'); const SteamCommunity = require('../index.js');
const SteamSession = require('steam-session');
const ReadLine = require('readline');
const FS = require('fs');
var community = new SteamCommunity(); const EResult = SteamCommunity.EResult;
var rl = ReadLine.createInterface({
"input": process.stdin,
"output": process.stdout
});
rl.question("Username: ", function(accountName) { let g_AbortPromptFunc = null;
rl.question("Password: ", function(password) {
doLogin(accountName, password); 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();
}); });
});
function doLogin(accountName, password, authCode, captcha) { session.on('timeout', () => {
community.login({ abortPrompt();
"accountName": accountName, console.log('This login attempt has timed out.');
"password": password, });
"authCode": authCode,
"captcha": captcha session.on('error', (err) => {
}, function(err, sessionID, cookies, steamguard) { abortPrompt();
if(err) {
if(err.message == 'SteamGuardMobile') { // This should ordinarily not happen. This only happens in case there's some kind of unexpected error while
console.log("This account already has two-factor authentication enabled."); // 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(); process.exit();
return; return;
} }
if(err.message == 'SteamGuard') { if (err.eresult == EResult.RateLimitExceeded) {
console.log("An email has been sent to your address at " + err.emaildomain); console.log('Error: RateLimitExceeded. Try again later.');
rl.question("Steam Guard Code: ", function (code) { process.exit();
doLogin(accountName, password, code);
});
return;
}
if(err.message == 'CAPTCHA') {
console.log(err.captchaurl);
rl.question("CAPTCHA: ", function(captchaInput) {
doLogin(accountName, password, authCode, captchaInput);
});
return; return;
} }
@@ -51,57 +101,82 @@ function doLogin(accountName, password, authCode, captcha) {
return; return;
} }
console.log("Logged on!"); if (response.status != EResult.OK) {
community.enableTwoFactor(function(err, response) { console.log(`Error: Status ${response.status}`);
if(err) {
if(err.eresult == 2) {
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 == 84) {
console.log("Error: RateLimitExceeded. Try again later.");
process.exit();
return;
}
console.log(err);
process.exit();
return;
}
if(response.status != 1) {
console.log("Error: Status " + response.status);
process.exit();
return;
}
console.log("Writing secrets to twofactor_" + community.steamID.getSteamID64() + ".json");
console.log("Revocation code: " + response.revocation_code);
fs.writeFile("twofactor_" + community.steamID.getSteamID64() + ".json", JSON.stringify(response, null, "\t"));
promptActivationCode(response);
});
});
}
function promptActivationCode(response) {
rl.question("SMS Code: ", function(smsCode) {
community.finalizeTwoFactor(response.shared_secret, smsCode, function(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(); 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');
}

758
index.js
View File

@@ -1,445 +1,507 @@
require('@doctormckay/stats-reporter').setup(require('./package.json')); 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');
var Request = require('request'); const Helpers = require('./components/helpers.js');
var RSA = require('node-bignumber').Key; const Package = require('./package.json');
var hex2b64 = require('node-bignumber').hex2b64;
var SteamID = require('steamid');
const USER_AGENT = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/56.0.2924.87 Safari/537.36"; 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';
require('util').inherits(SteamCommunity, require('events').EventEmitter); Util.inherits(SteamCommunity, EventEmitter);
module.exports = SteamCommunity; module.exports = SteamCommunity;
SteamCommunity.SteamID = SteamID; SteamCommunity.SteamID = SteamID;
SteamCommunity.ConfirmationType = require('./resources/EConfirmationType.js'); SteamCommunity.EConfirmationType = require('./resources/EConfirmationType.js');
SteamCommunity.EResult = require('./resources/EResult.js'); SteamCommunity.EResult = require('./resources/EResult.js');
SteamCommunity.ESharedFileType = require('./resources/ESharedFileType.js');
SteamCommunity.EFriendRelationship = require('./resources/EFriendRelationship.js');
/**
*
* @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) { function SteamCommunity(options) {
options = options || {}; options = options || {};
this._jar = Request.jar(); this.packageName = Package.name;
this.packageVersion = Package.version;
this._jar = new StdLib.HTTP.CookieJar();
this._captchaGid = -1; this._captchaGid = -1;
this._httpRequestID = 0; this._httpRequestID = 0;
this.chatState = SteamCommunity.ChatState.Offline;
var defaults = { let defaultHeaders = {
"jar": this._jar, 'user-agent': USER_AGENT
"timeout": options.timeout || 50000,
"gzip": true,
"headers": {
"User-Agent": options.userAgent || USER_AGENT
}
}; };
if (typeof options == "string") { // Apply the user's custom default headers
options = { for (let i in (options.defaultHttpHeaders || {})) {
localAddress: options // 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; this._options = options;
if (options.localAddress) {
defaults.localAddress = options.localAddress;
}
this.request = options.request || Request.defaults({"forever": true}); // "forever" indicates that we want a keep-alive agent
this.request = this.request.defaults(defaults);
// English // English
this._setCookie(Request.cookie('Steam_Language=english')); this._setCookie('Steam_Language=english');
// UTC // UTC
this._setCookie(Request.cookie('timezoneOffset=0,0')); this._setCookie('timezoneOffset=0,0');
} }
SteamCommunity.prototype.login = function(details, callback) { /**
if (!details.accountName || !details.password) { * @param {object} details
throw new Error("Missing either accountName or password to login; both are needed"); * @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');
} }
if (details.steamguard) { // eslint-disable-next-line no-async-promise-executor
var parts = details.steamguard.split('||'); return new Promise(async (resolve, reject) => {
this._setCookie(Request.cookie('steamMachineAuth' + parts[0] + '=' + encodeURIComponent(parts[1])), true); let platformType = details.authTokenPlatformType || EAuthTokenPlatformType.MobileApp;
} let session = new LoginSession(platformType, {
httpProxy: this._options.httpProxy
var self = this; });
// Delete the cache session.on('authenticated', async () => {
delete self._profileURL; try {
let cookies = await session.getWebCookies();
this.setCookies(cookies);
// headers required to convince steam that we're logging in from a mobile device so that we can get the oAuth data if (platformType == EAuthTokenPlatformType.MobileApp) {
var mobileHeaders = { this.setMobileAppAccessToken(session.accessToken);
"X-Requested-With": "com.valvesoftware.android.steam.community",
"Referer": "https://steamcommunity.com/mobilelogin?oauth_client_id=DE45CD61&oauth_scope=read_profile%20write_profile%20read_client%20write_client",
"User-Agent": "Mozilla/5.0 (Linux; U; Android 4.1.1; en-us; Google Nexus 4 - 4.1.1 - API 16 - 768x1280 Build/JRO03S) AppleWebKit/534.30 (KHTML, like Gecko) Version/4.0 Mobile Safari/534.30",
"Accept": "text/javascript, text/html, application/xml, text/xml, */*"
};
this._setCookie(Request.cookie("mobileClientVersion=0 (2.1.3)"));
this._setCookie(Request.cookie("mobileClient=android"));
this.httpRequestPost("https://steamcommunity.com/login/getrsakey/", {
"form": {"username": details.accountName},
"headers": mobileHeaders,
"json": true
}, function(err, response, body) {
// Remove the mobile cookies
if (err) {
deleteMobileCookies();
callback(err);
return;
}
if(!body.publickey_mod || !body.publickey_exp) {
deleteMobileCookies();
callback(new Error("Invalid RSA key received"));
return;
}
var key = new RSA();
key.setPublic(body.publickey_mod, body.publickey_exp);
self.httpRequestPost({
"uri": "https://steamcommunity.com/login/dologin/",
"json": true,
"form": {
"captcha_text": details.captcha || "",
"captchagid": self._captchaGid,
"emailauth": details.authCode || "",
"emailsteamid": "",
"password": hex2b64(key.encrypt(details.password)),
"remember_login": "true",
"rsatimestamp": body.timestamp,
"twofactorcode": details.twoFactorCode || "",
"username": details.accountName,
"oauth_client_id": "DE45CD61",
"oauth_scope": "read_profile write_profile read_client write_client",
"loginfriendlyname": "#login_emailauth_friendlyname_mobile",
"donotcache": Date.now()
},
"headers": mobileHeaders
}, function(err, response, body) {
deleteMobileCookies();
if (err) {
callback(err);
return;
}
var error;
if(!body.success && body.emailauth_needed) {
// Steam Guard (email)
error = new Error("SteamGuard");
error.emaildomain = body.emaildomain;
callback(error);
} else if(!body.success && body.requires_twofactor) {
// Steam Guard (app)
callback(new Error("SteamGuardMobile"));
} else if(!body.success && body.captcha_needed && body.message.match(/Please verify your humanity/)) {
error = new Error("CAPTCHA");
error.captchaurl = "https://steamcommunity.com/login/rendercaptcha/?gid=" + body.captcha_gid;
self._captchaGid = body.captcha_gid;
callback(error);
} else if(!body.success) {
callback(new Error(body.message || "Unknown error"));
} else if(!body.oauth) {
callback(new Error("Malformed response"));
} else {
var sessionID = generateSessionID();
var oAuth = JSON.parse( body.oauth );
self._setCookie(Request.cookie('sessionid=' + sessionID));
self.steamID = new SteamID(oAuth.steamid);
self.oAuthToken = oAuth.oauth_token;
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;
}
} }
self.setCookies(cookies); // TODO set refresh token for session keep-alive
callback(null, sessionID, cookies, steamguard, oAuth.oauth_token); 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);
} }
}, "steamcommunity"); });
}, "steamcommunity");
function deleteMobileCookies() { session.on('timeout', () => {
var cookie = Request.cookie('mobileClientVersion='); // This really shouldn't happen
cookie.expires = new Date(0); reject(new Error('Login attempt timed out'));
self._setCookie(cookie); });
session.on('error', reject);
cookie = Request.cookie('mobileClient='); try {
cookie.expires = new Date(0); let startResult = await session.startWithCredentials({
self._setCookie(cookie); accountName: details.accountName,
} password: details.password,
}; steamGuardCode: details.twoFactorCode || details.authCode
});
SteamCommunity.prototype.oAuthLogin = function(steamguard, token, callback) { if (!startResult.actionRequired) {
steamguard = steamguard.split('||'); return; // 'authenticated' should get emitted soon
var steamID = new SteamID(steamguard[0]); }
var self = this; session.cancelLoginAttempt();
this.httpRequestPost({
"uri": "https://api.steampowered.com/IMobileAuthService/GetWGToken/v1/", if (startResult.validActions.some(a => a.type == EAuthSessionGuardType.EmailCode)) {
"form": { return reject(new Error('SteamGuard'));
"access_token": token }
},
"json": true if (startResult.validActions.some(a => a.type == EAuthSessionGuardType.DeviceCode)) {
}, function(err, response, body) { return reject(new Error('SteamGuardMobile'));
if (err) { }
callback(err);
return; let validActions = startResult.validActions.map(a => a.type).join(', ');
return reject(new Error(`Unexpected guard action(s) ${validActions}`));
} catch (ex) {
reject(ex);
} }
if(!body.response || !body.response.token || !body.response.token_secure) {
callback(new Error("Malformed response"));
return;
}
var cookies = [
'steamLogin=' + encodeURIComponent(steamID.getSteamID64() + '||' + body.response.token),
'steamLoginSecure=' + encodeURIComponent(steamID.getSteamID64() + '||' + body.response.token_secure),
'steamMachineAuth' + steamID.getSteamID64() + '=' + steamguard[1],
'sessionid=' + self.getSessionID()
];
self.setCookies(cookies);
callback(null, self.getSessionID(), cookies);
}, "steamcommunity");
};
SteamCommunity.prototype._setCookie = function(cookie, secure) {
var protocol = secure ? "https" : "http";
cookie.secure = !!secure;
this._jar.setCookie(cookie.clone(), protocol + "://steamcommunity.com");
this._jar.setCookie(cookie.clone(), protocol + "://store.steampowered.com");
this._jar.setCookie(cookie.clone(), protocol + "://help.steampowered.com");
};
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._setCookie(Request.cookie(cookie), !!(cookieName.match(/^steamMachineAuth/) || cookieName.match(/Secure$/)));
}); });
}; };
SteamCommunity.prototype.getSessionID = function() { /**
var cookies = this._jar.getCookieString("http://steamcommunity.com").split(';'); * Get a token that can be used to log onto Steam using steam-user.
for(var i = 0; i < cookies.length; i++) { * @param {function} [callback]
var match = cookies[i].trim().match(/([^=]+)=(.+)/); * @return Promise<{steamID: SteamID, accountName: string, webLogonToken: string}>
if(match[1] == 'sessionid') { */
return decodeURIComponent(match[2]); 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
});
});
};
/**
* 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');
};
/**
* 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(); cookies.forEach((cookie) => {
this._setCookie(Request.cookie('sessionid=' + sessionID)); 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; return sessionID;
}; };
function generateSessionID() { /**
return require('crypto').randomBytes(12).toString('hex'); * @param {string} pin
} * @param {function} [callback]
* @return Promise<void>
*/
SteamCommunity.prototype.parentalUnlock = function(pin, callback) { SteamCommunity.prototype.parentalUnlock = function(pin, callback) {
var self = this; let sessionID = this.getSessionID();
this.httpRequestPost("https://steamcommunity.com/parental/ajaxunlock", { return StdLib.Promises.callbackPromise(null, callback, true, async (resolve, reject) => {
"json": true, let {jsonBody} = await this.httpRequest({
"form": { method: 'POST',
"pin": pin url: 'https://steamcommunity.com/parental/ajaxunlock',
} form: {
}, function(err, response, body) { pin: pin,
if(!callback) { sessionid: sessionID
return; },
} source: 'steamcommunity'
});
if (err) {
callback(err);
return;
}
if (!body || typeof body.success !== 'boolean') {
callback("Invalid response");
return;
}
if (!body.success) {
switch (body.eresult) {
case 15:
callback("Incorrect PIN");
break;
case 25: if (!jsonBody || typeof jsonBody.success !== 'boolean') {
callback("Too many invalid PIN attempts"); return reject('Invalid response');
break; }
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: default:
callback("Error " + body.eresult); return reject('Error ' + jsonBody.eresult);
} }
return;
} }
callback(); resolve();
}.bind(this), "steamcommunity"); });
}; };
/**
* @param {function} [callback]
* @return Promise<object>
*/
SteamCommunity.prototype.getNotifications = function(callback) { SteamCommunity.prototype.getNotifications = function(callback) {
var self = this; return StdLib.Promises.callbackPromise(null, callback, false, async (resolve, reject) => {
this.httpRequestGet({ let {jsonBody} = await this.httpRequest({
"uri": "https://steamcommunity.com/actions/GetNotificationCounts", method: 'GET',
"json": true url: 'https://steamcommunity.com/actions/GetNotificationCounts',
}, function(err, response, body) { source: 'steamcommunity'
if (err) { });
callback(err);
return; if (!jsonBody || !jsonBody.notifications) {
return reject(new Error('Malformed response'));
} }
if (!body || !body.notifications) { let notifications = {
callback(new Error("Malformed response")); trades: jsonBody.notifications[1] || 0,
return; gameTurns: jsonBody.notifications[2] || 0,
} moderatorMessages: jsonBody.notifications[3] || 0,
comments: jsonBody.notifications[4] || 0,
var notifications = { items: jsonBody.notifications[5] || 0,
"trades": body.notifications[1] || 0, invites: jsonBody.notifications[6] || 0,
"gameTurns": body.notifications[2] || 0,
"moderatorMessages": body.notifications[3] || 0,
"comments": body.notifications[4] || 0,
"items": body.notifications[5] || 0,
"invites": body.notifications[6] || 0,
// dunno about 7 // dunno about 7
"gifts": body.notifications[8] || 0, gifts: jsonBody.notifications[8] || 0,
"chat": body.notifications[9] || 0, chat: jsonBody.notifications[9] || 0,
"helpRequestReplies": body.notifications[10] || 0, helpRequestReplies: jsonBody.notifications[10] || 0,
"accountAlerts": body.notifications[11] || 0 accountAlerts: jsonBody.notifications[11] || 0
}; };
callback(null, notifications); resolve(notifications);
}, "steamcommunity"); });
}; };
/**
* @param {function} [callback]
* @return Promise<void>
*/
SteamCommunity.prototype.resetItemNotifications = function(callback) { SteamCommunity.prototype.resetItemNotifications = function(callback) {
var self = this; return StdLib.Promises.callbackPromise(null, callback, true, async (resolve, reject) => {
this.httpRequestGet("https://steamcommunity.com/my/inventory", function(err, response, body) { await this.httpRequest({
if(!callback) { method: 'GET',
return; url: 'https://steamcommunity.com/my/inventory',
} source: 'steamcommunity'
});
callback(err || null); resolve();
}, "steamcommunity"); });
}; };
/**
* @param {function} [callback]
* @return Promise<{loggedIn: boolean, familyView: boolean}>
*/
SteamCommunity.prototype.loggedIn = function(callback) { SteamCommunity.prototype.loggedIn = function(callback) {
this.httpRequestGet({ return StdLib.Promises.callbackPromise(['loggedIn', 'familyView'], callback, false, async (resolve, reject) => {
"uri": "https://steamcommunity.com/my", let result = await this.httpRequest({
"followRedirect": false, method: 'GET',
"checkHttpError": false url: 'https://steamcommunity.com/my',
}, function(err, response, body) { followRedirect: false,
if(err || (response.statusCode != 302 && response.statusCode != 403)) { checkHttpError: false,
callback(err || new Error("HTTP error " + response.statusCode)); source: 'steamcommunity'
return; });
if (result.statusCode != 302 && result.statusCode != 403) {
return reject(new Error(`HTTP error ${result.statusCode}`));
} }
if(response.statusCode == 403) { if (result.statusCode == 403) {
callback(null, true, true); // TODO check response body to see if this is an akamai block
return; return resolve({
loggedIn: true,
familyView: true
});
} }
callback(null, !!response.headers.location.match(/steamcommunity\.com(\/(id|profiles)\/[^\/]+)\/?/), false); return resolve({
}, "steamcommunity"); loggedIn: !!result.headers.location.match(/steamcommunity\.com(\/(id|profiles)\/[^/]+)\/?/),
familyView: false
});
});
}; };
/**
* @param {function} [callback]
* @return Promise<{url: string, token: string}>
*/
SteamCommunity.prototype.getTradeURL = function(callback) { SteamCommunity.prototype.getTradeURL = function(callback) {
this._myProfile("/tradeoffers/privacy", null, (err, response, body) => { return StdLib.Promises.callbackPromise(['url', 'token'], callback, false, async (resolve, reject) => {
var match = body.match(/https?:\/\/(www.)?steamcommunity.com\/tradeoffer\/new\/?\?partner=\d+(&|&amp;)token=([a-zA-Z0-9-_]+)/); let {textBody} = await this._myProfile('tradeoffers/privacy');
if (match) {
var token = match[3]; let match = textBody.match(/https?:\/\/(www.)?steamcommunity.com\/tradeoffer\/new\/?\?partner=\d+(&|&amp;)token=([a-zA-Z0-9-_]+)/);
callback(null, match[0], token); if (!match) {
} else { return reject(new Error('Malformed response'));
callback(new Error("Malformed response"));
} }
}, "tradeoffermanager");
let token = match[3];
resolve({
url: match[0],
token
});
});
}; };
SteamCommunity.prototype._myProfile = function(endpoint, form, callback) { /**
var self = this; * @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 (this._profileURL) { if (!textBody || typeof textBody !== 'string' || textBody.length < 3 || textBody.indexOf('"') !== 0) {
completeRequest(this._profileURL); return reject(new Error('Malformed response'));
} else {
this.httpRequest("https://steamcommunity.com/my", {"followRedirect": false}, function(err, response, body) {
if(err || response.statusCode != 302) {
callback(err || "HTTP error " + response.statusCode);
return;
}
var match = response.headers.location.match(/steamcommunity\.com(\/(id|profiles)\/[^\/]+)\/?/);
if(!match) {
callback(new Error("Can't get profile URL"));
return;
}
self._profileURL = match[1];
setTimeout(function () {
delete self._profileURL; // delete the cache
}, 60000);
completeRequest(match[1]);
}, "steamcommunity");
}
function completeRequest(url) {
var options = {
"uri": "https://steamcommunity.com" + url + "/" + endpoint,
"method": "GET"
};
if (form) {
options.method = "POST";
options.form = form;
} }
self.httpRequest(options, callback, "steamcommunity"); 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/http.js');
require('./components/chat.js');
require('./components/profile.js'); require('./components/profile.js');
require('./components/market.js'); require('./components/market.js');
require('./components/groups.js'); require('./components/groups.js');
require('./components/users.js'); require('./components/users.js');
require('./components/inventoryhistory.js'); require('./components/sharedfiles.js');
require('./components/webapi.js'); require('./components/webapi.js');
require('./components/twofactor.js'); require('./components/twofactor.js');
require('./components/confirmations.js'); require('./components/confirmations.js');
require('./components/help.js');
require('./classes/CMarketItem.js'); require('./classes/CMarketItem.js');
require('./classes/CMarketSearchResult.js'); require('./classes/CMarketSearchResult.js');
require('./classes/CSteamGroup.js'); require('./classes/CSteamGroup.js');
require('./classes/CSteamSharedFile.js');
require('./classes/CSteamUser.js'); require('./classes/CSteamUser.js');
/** /**
@callback SteamCommunity~genericErrorCallback @callback SteamCommunity~genericErrorCallback
@param {Error|null} err - An Error object on failure, or null on success @param {Error|null} err - An Error object on failure, or null on success
*/ */

View File

@@ -1,6 +1,7 @@
{ {
"name": "steamcommunity", "name": "steamcommunity",
"version": "3.33.1", "version": "4.0.0-dev",
"private": true,
"description": "Provides an interface for logging into and interacting with the Steam Community website", "description": "Provides an interface for logging into and interacting with the Steam Community website",
"keywords": [ "keywords": [
"steam", "steam",
@@ -11,21 +12,32 @@
"url": "https://github.com/DoctorMcKay/node-steamcommunity/issues" "url": "https://github.com/DoctorMcKay/node-steamcommunity/issues"
}, },
"license": "MIT", "license": "MIT",
"author": {
"name": "Alex Corn",
"email": "mckay@doctormckay.com",
"url": "https://www.doctormckay.com"
},
"repository": { "repository": {
"type": "git", "type": "git",
"url": "https://github.com/DoctorMcKay/node-steamcommunity.git" "url": "https://github.com/DoctorMcKay/node-steamcommunity.git"
}, },
"dependencies": { "dependencies": {
"@doctormckay/stats-reporter": "^1.0.2", "@doctormckay/stdlib": "^2.6.0",
"request": "^2.81.0",
"node-bignumber": "^1.2.1",
"steamid": "^1.0.0",
"xml2js": "^0.4.11",
"cheerio": "0.22.0", "cheerio": "0.22.0",
"async": "^2.1.4", "image-size": "^0.8.2",
"steam-totp": "^1.3.0" "steam-session": "^1.2.4",
"steam-totp": "^2.1.0",
"steamid": "^2.0.0",
"tough-cookie": "^4.0.0",
"xml2js": "^0.6.0"
}, },
"engines": { "engines": {
"node": ">=4.0.0" "node": ">=14.0.0"
},
"devDependencies": {
"eslint": "^7.31.0"
},
"scripts": {
"lint": "npx eslint . --ext .js,.jsx,.ts,.tsx"
} }
} }

View File

@@ -1,14 +0,0 @@
/**
* @enum EChatState
*/
module.exports = {
"Offline": 0,
"LoggingOn": 1,
"LogOnFailed": 2,
"LoggedOn": 3,
"0": "Offline",
"1": "LoggingOn",
"2": "LogOnFailed",
"3": "LoggedOn"
};

View File

@@ -1,3 +1,5 @@
/* eslint-disable */
/** /**
* @enum EConfirmationType * @enum EConfirmationType
*/ */

View 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",
};

View File

@@ -1,3 +1,5 @@
/* eslint-disable */
/** /**
* @enum EPersonaState * @enum EPersonaState
*/ */
@@ -9,7 +11,7 @@ module.exports = {
"Snooze": 4, "Snooze": 4,
"LookingToTrade": 5, "LookingToTrade": 5,
"LookingToPlay": 6, "LookingToPlay": 6,
"Max": 7, "Invisible": 7,
// Value-to-name mapping for convenience // Value-to-name mapping for convenience
"0": "Offline", "0": "Offline",
@@ -19,5 +21,5 @@ module.exports = {
"4": "Snooze", "4": "Snooze",
"5": "LookingToTrade", "5": "LookingToTrade",
"6": "LookingToPlay", "6": "LookingToPlay",
"7": "Max", "7": "Invisible",
}; };

View File

@@ -1,10 +1,13 @@
/* eslint-disable */
/** /**
* @enum EPersonaStateFlag * @enum EPersonaStateFlag
*/ */
module.exports = { module.exports = {
"HasRichPresence": 1, "HasRichPresence": 1,
"InJoinableGame": 2, "InJoinableGame": 2,
"Golden": 4, // removed "no longer has any effect" "Golden": 4,
"RemotePlayTogether": 8,
"OnlineUsingWeb": 256, // removed "renamed to ClientTypeWeb" "OnlineUsingWeb": 256, // removed "renamed to ClientTypeWeb"
"ClientTypeWeb": 256, "ClientTypeWeb": 256,
"OnlineUsingMobile": 512, // removed "renamed to ClientTypeMobile" "OnlineUsingMobile": 512, // removed "renamed to ClientTypeMobile"
@@ -14,14 +17,18 @@ module.exports = {
"OnlineUsingVR": 2048, // removed "renamed to ClientTypeVR" "OnlineUsingVR": 2048, // removed "renamed to ClientTypeVR"
"ClientTypeVR": 2048, "ClientTypeVR": 2048,
"LaunchTypeGamepad": 4096, "LaunchTypeGamepad": 4096,
"LaunchTypeCompatTool": 8192,
// Value-to-name mapping for convenience // Value-to-name mapping for convenience
"1": "HasRichPresence", "1": "HasRichPresence",
"2": "InJoinableGame", "2": "InJoinableGame",
"4": "Golden", "4": "Golden",
"8": "RemotePlayTogether",
"256": "ClientTypeWeb", "256": "ClientTypeWeb",
"512": "ClientTypeMobile", "512": "ClientTypeMobile",
"1024": "ClientTypeTenfoot", "1024": "ClientTypeTenfoot",
"2048": "ClientTypeVR", "2048": "ClientTypeVR",
"4096": "LaunchTypeGamepad", "4096": "LaunchTypeGamepad",
"8192": "LaunchTypeCompatTool",
}; };

View File

@@ -1,3 +1,6 @@
/* eslint-disable */
// Auto-generated by generate-enums script on Thu Jul 29 2021 04:43:52 GMT-0400 (Eastern Daylight Time)
/** /**
* @enum EResult * @enum EResult
*/ */
@@ -119,6 +122,18 @@ module.exports = {
"GSLTExpired": 106, "GSLTExpired": 106,
"InsufficientFunds": 107, "InsufficientFunds": 107,
"TooManyPending": 108, "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 // Value-to-name mapping for convenience
"0": "Invalid", "0": "Invalid",
@@ -229,4 +244,15 @@ module.exports = {
"106": "GSLTExpired", "106": "GSLTExpired",
"107": "InsufficientFunds", "107": "InsufficientFunds",
"108": "TooManyPending", "108": "TooManyPending",
"109": "NoSiteLicensesFound",
"110": "WGNetworkSendExceeded",
"111": "AccountNotFriends",
"112": "LimitedUserAccount",
"113": "CantRemoveItem",
"114": "AccountHasBeenDeleted",
"115": "AccountHasAnExistingUserCancelledLicense",
"116": "DeniedDueToCommunityCooldown",
"117": "NoLauncherSpecified",
"118": "MustAgreeToSSA",
"119": "LauncherMigrated",
}; };

View 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"
};