Compare commits

...

357 Commits

Author SHA1 Message Date
Alex Corn
2a12174a7c 3.50.0 2026-03-27 04:32:44 -04:00
DoctorMcKay
caa76ee84b Merge pull request #365 from DarkGL/feat/asset-properties
feat: Add asset_properties support to getInventoryContents
2026-03-27 04:29:47 -04:00
Rafal Wiecek
bc8893fedd feat: add asset properties to inventory items 2026-03-16 15:52:32 +01:00
Alex Corn
d3e90f6fd3 3.49.0 2025-07-19 05:00:47 -04:00
Alex Corn
a61b50b7a2 Added acknowledgeTradeProtection method 2025-07-19 04:59:15 -04:00
Alex Corn
39fe4a4106 Merge remote-tracking branch 'origin/master'
# Conflicts:
#	package.json
2025-07-19 04:49:23 -04:00
Alex Corn
0d991c2655 3.48.8 2025-05-28 05:23:39 -04:00
Alex Corn
0a307de9b9 Prevent descriptions overwriting base econ item properties 2025-05-28 05:23:25 -04:00
Alex Corn
c0b8dcf3a2 3.48.7 2025-05-27 20:27:10 -04:00
DoctorMcKay
dcf00b4ea1 Merge pull request #354 from metzz1/master
fix: steam now only accepts 1000 <
2025-05-27 20:26:40 -04:00
metzz
ee0cc34512 fix: steam now only accepts 1000 < 2025-05-28 03:08:12 +03:00
Alex Corn
c52533ea06 3.48.6 2025-02-12 03:16:04 -05:00
Alex Corn
67774f86f7 Handle "empty" CS2 inventories that aren't actually empty 2025-02-12 03:15:54 -05:00
Alex Corn
2f03e09f50 Require steam-session 1.9.1 2025-02-12 03:05:15 -05:00
Alex Corn
2868b1f45f Fixed malformed sessionid returned in login callback 2025-02-12 03:05:07 -05:00
Alex Corn
ac222ef8c3 Add origin header to all non-GET requests 2025-02-12 03:04:45 -05:00
Alex Corn
4b8799ddd4 3.48.5 2024-11-02 02:54:44 -04:00
Alex Corn
f02f85a2aa Don't error out if a CS inventory has no visible items 2024-11-02 02:54:16 -04:00
Alex Corn
4948cbeac0 Merge remote-tracking branch 'origin/master'
# Conflicts:
#	package.json
2024-11-02 02:49:14 -04:00
Alex Corn
1c1ff82543 3.48.4 2024-10-05 18:50:47 -04:00
Alex Corn
63015259af Updated readme 2024-10-05 18:47:05 -04:00
Alex Corn
ad67805ad9 Prevent committing twofactor files to git 2024-10-05 18:45:26 -04:00
DoctorMcKay
3347d87f29 Merge pull request #347 from benschool/master
Fixes cache_expiration restore for CS2 Items
2024-10-05 18:41:48 -04:00
Benjamin Tyler
c97351543a Fixes cache_expiration restore for CS2 Items
As of Oct 2024, CS2 Item owner_description has changed from "Tradable After" to "Tradable/Marketable After".
2024-10-04 16:04:47 +01:00
DoctorMcKay
1067d4572e Merge pull request #341 from makss/reduce_redirects
Reduce Steam redirects
2024-04-29 10:21:41 -04:00
makss
49a7165052 Reduce Steam redirects 2024-04-29 16:40:01 +03:00
Alex Corn
0fb2798424 3.48.3 2024-04-18 06:04:25 -04:00
DoctorMcKay
e16136866c Merge pull request #340 from nolddor/patch-1
fix: getUserInventoryContents() make unnecesary calls
2024-04-18 06:04:06 -04:00
Jack Nolddor
cae85433f9 fix: getUserInventoryContents() make unnecesary calls
Steam inventory limits are back to normal there is no reason to just gather 2k items instead 5k max allowed by API
2024-04-18 08:14:14 +02:00
Alex Corn
ba6e29c935 3.48.2 2024-01-18 06:12:30 -05:00
Alex Corn
de95e25867 Fixed steamID not being properly set when cookies have a domain attribute 2024-01-18 06:12:18 -05:00
Alex Corn
e1820efeca 3.48.1 2024-01-18 05:38:41 -05:00
Alex Corn
7cee24199f Fixed login issue caused by differing tokens on different domains 2024-01-18 05:38:06 -05:00
Alex Corn
90eb4d38eb 3.48.0 2023-12-02 21:23:47 -05:00
Alex Corn
b58745c8b7 Added createWebApiKey method 2023-12-02 20:26:52 -05:00
Alex Corn
53154043a3 Use updated AddAuthenticator parameters
The old way still seems to work, but this is what the official mobile app sends now
2023-11-10 20:50:12 -05:00
Alex Corn
6c77c5231c Updated enable and disable_twofactor examples to not use steam-session 2023-11-10 20:49:48 -05:00
Alex Corn
7c564c1453 3.47.1 2023-10-20 00:40:31 -04:00
Alex Corn
b854d6d4d4 Use custom user agent for logins 2023-10-20 00:39:49 -04:00
Alex Corn
67867978f8 Get default user agent string from @doctormckay/user-agents 2023-10-20 00:39:38 -04:00
Alex Corn
e1af63e171 Use steam-session for logins 2023-10-19 22:12:40 -04:00
Alex Corn
e4da22c464 3.47.0 2023-10-01 21:39:33 -04:00
Alex Corn
0f91adf819 Updated xml2js dependency version 2023-10-01 21:39:16 -04:00
Alex Corn
d51c171a48 Rename clanid argument to curatorId 2023-10-01 21:34:04 -04:00
Alex Corn
50276c3a0c Explicitly define which files get published to npm 2023-10-01 21:33:39 -04:00
Alex Corn
b5b50cec50 Ignore dev/ 2023-10-01 21:30:28 -04:00
Alex Corn
cf4b474c70 Added steamID coalescing helper, although currently unused 2023-10-01 21:30:12 -04:00
3urobeat
bf2b4601ee Add user/workshop/curator follow & unfollow support (#320)
* Add follow & unfollow user functions

* Add follow & unfollow to CSteamUser object methods

* Add support for following & unfollowing curators

* Correctly parse for eresult on error

* Use eresultError() helper
2023-10-01 21:22:44 -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
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
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
7ae8a97a0f Slight refactor for help.js component 2021-07-22 23:58:47 -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
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
fbfff1398b Merge remote-tracking branch 'origin/master'
# Conflicts:
#	.idea/encodings.xml
2019-02-14 00:29:44 -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
Alexander Corn
b45e8d97ed 3.33.1 2017-10-21 16:52:11 -04:00
Alex Corn
6abb14a9bc Merge pull request #183 from borianm/patch-1
Fix for issue #182
2017-10-21 16:48:23 -04:00
Alexander Corn
6049d7a259 Added deprecation notice to getInventoryHistory 2017-10-20 19:28:26 -04:00
AirboN
e0320b27d7 Fixed issue with undefined author in events 2017-10-17 00:48:06 +02:00
AirboN
7986a87b3f Fixed issue with undefined author in events 2017-10-17 00:42:26 +02:00
Alexander Corn
cf7e5f8995 3.33.0 2017-10-06 12:01:10 -04:00
Alexander Corn
a03dad9507 Added getTradeURL method 2017-10-02 14:55:29 -04:00
Alexander Corn
7da7ecfc56 3.32.3 2017-09-29 00:13:17 -04:00
Alex Corn
6bf3f26f2f Merge pull request #175 from Visagalis/patch-1
Typo/bug in GetGroupHistory code.
2017-09-29 00:13:00 -04:00
Jason Vaillancourt
dc2e5d09a7 Merge branch 'master' into groupcomments 2017-08-20 20:13:41 -04:00
Alexander Corn
d716e8a494 3.32.2 2017-08-17 13:12:44 -04:00
Alexander Corn
458a179852 Fixed reference to undefined self 2017-08-17 13:12:36 -04:00
Alexander Corn
f5d76527ba 3.32.1 2017-08-15 21:43:14 -04:00
Alexander Corn
5875f47ebc Ignore chat friendstate responses if we no longer care 2017-08-15 21:43:10 -04:00
Julius
d7095aa7c5 Typo/bug in GetGroupHistory code.
Typo which makes group history objects to contain incorrect actor (it contains same actor as user).
2017-08-08 21:52:12 +03:00
Alexander Corn
03e0a1bdac 3.32.0 2017-08-06 19:18:43 -04:00
Alexander Corn
a51973c9b9 Added CSteamGroup methods for group-join requests 2017-08-06 19:11:21 -04:00
Alexander Corn
96d87ee8e6 Added methods to deal with Steam group join requests 2017-08-06 19:08:45 -04:00
Alexander Corn
7beef548dc Merge pull request #174 from Unsigno/master
Updated getUserInventoryContexts
2017-07-11 12:32:27 -04:00
Unsigno
6d9ebabb17 Updated getUserInventoryContexts to return 0 inventories and handle private profile error 2017-07-11 14:02:01 +02:00
Alexander Corn
5d5d8548af 3.31.0 2017-06-15 10:06:54 -04:00
Alexander Corn
b5142ad0cc Added language argument to getUserInventoryContents 2017-06-15 10:04:37 -04:00
Alexander Corn
3045a5d0cc Added stats-reporter integration 2017-06-15 10:03:14 -04:00
Alexander Corn
2dcb123761 Update request dependency version
(make david-dm happy)
2017-06-11 13:26:52 -04:00
Alexander Corn
bd29d598b1 Throw Error if using confirmations without being logged in 2017-06-02 23:35:23 -04:00
Alexander Corn
7e39715427 Merge pull request #168 from morethanwords/patch-1
Update getUserInventoryContents
2017-04-21 14:23:25 -04:00
Eduard Kuzmenko
a97edfd492 Update users.js 2017-04-21 15:46:20 +03:00
Eduard Kuzmenko
68b1f21655 Update getUserInventoryContents
There will be a re-loop if description of the item not the last one in descriptions (and quickDescriptionLookup will rewrite everytime)
2017-04-21 15:26:45 +03:00
Alexander Corn
b4772baf39 3.30.7 2017-03-27 14:42:46 -04:00
Alexander Corn
47dc22ca0d Handle cases where inventories are private in getUserInventoryContexts 2017-03-27 14:39: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
Alexander Corn
acbd71666b Merge pull request #165 from Aareksio/patch-1
Check if instance is logged in before comparing steamids (fixes #164)
2017-03-06 11:35:23 -05:00
Arkadiusz Sygulski
3c35026a32 Check if instance is logged in before comparing steamids (fixes #164) 2017-03-06 16:02:20 +01:00
Alexander Corn
aca375d9da Give different error message if family view unlock failed for rate-limit 2017-03-04 22:30:25 -05:00
Jarzon
91285ba1c7 Merge branch 'master' into groupcomments 2017-03-04 09:04:24 -05:00
Alexander Corn
758b749487 3.30.6 2017-02-25 19:12:42 -05:00
Alexander Corn
a9b16cef1e Fixed crash no non-string 403 responses
https://dev.doctormckay.com/topic/845-responsebodymatch-is-not-a-function/
2017-02-25 19:12:34 -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
Alexander Corn
4542793175 3.30.5 2017-02-12 02:22:43 -05:00
Alexander Corn
d18a0244ec Updated default user-agent to latest Chrome on Win10 x64 2017-02-12 02:22:17 -05:00
Alexander Corn
b1c772d9b9 Keep HTTP connections open between requests 2017-02-11 22:28:22 -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
Alexander Corn
b9ec1ccfb5 3.30.4 2017-01-18 15:51:44 -05:00
Alexander Corn
0c7c030f78 Fixed string in callback instead of Error (fixes #153) 2017-01-18 15:50:11 -05:00
Alexander Corn
e5f1f14aad Fixed empty inventory resulting in "Malformed response" (fixes #156)
Display Steam error message when available instead of "HTTP error 500"
2017-01-18 15:49:27 -05:00
Alexander Corn
0f72ea7d93 3.30.3 2017-01-04 00:10:40 -05:00
Alexander Corn
d1906a2c83 Fallback to the default "?" avatar if we don't have a hash (fixes #151) 2017-01-04 00:10:11 -05:00
Alexander Corn
3964af7ade Throw a descriptive error if accountName or password is missing in login
Closes #119
2017-01-04 00:07:33 -05:00
Alexander Corn
5a5c5373aa Bump dependency versions to make david happy 2017-01-04 00:03:18 -05:00
Alexander Corn
ec60e1b73e 3.30.2 2016-12-30 17:11:12 -05:00
Alexander Corn
a22a75d4e1 Merge pull request #150 from Vankxr/master
Check if requested inventory is ours.
2016-12-30 17:06:21 -05:00
Vankxr
313753bb05 Check if requested inventory is ours.
Should fix https://github.com/DoctorMcKay/node-steamcommunity/issues/149
2016-12-30 21:24:53 +00:00
Alexander Corn
459fe4d8b1 3.30.1 2016-12-15 00:53:46 -05:00
Alexander Corn
489c0441a5 Restore market_fee_app property to CEconItem for Steam Community items 2016-12-15 00:53:26 -05:00
Alexander Corn
d91e74d02a 3.30.0 2016-12-13 01:53:57 -05:00
Alexander Corn
d120d09970 Fixed tag data to be identical to old endpoint 2016-12-13 01:53:30 -05:00
Alexander Corn
84ce979a78 Fixed new inventory loading not working properly 2016-12-13 01:52:52 -05:00
Alexander Corn
88b0495386 Renamed getUserInventory, added back old one and marked it as deprecated 2016-12-13 01:32:17 -05:00
Alexander Corn
d367e8eae4 Added better paging controls for new inventory endpoint 2016-12-13 01:29:06 -05:00
Alexander Corn
9a3cf927c0 Fixed some more currency detection 2016-12-13 01:28:48 -05:00
Alexander Corn
56cdfff672 3.29.1 2016-12-13 01:07:12 -05:00
Alexander Corn
35e6dd16e7 Re-add "pos" property to inventory items for backwards compatibility 2016-12-13 01:07:00 -05:00
Alexander Corn
d0af8584c1 Send referer when fetching inventories 2016-12-13 00:50:29 -05:00
Alexander Corn
f1afcd1962 3.29.0 2016-12-13 00:46:23 -05:00
Alexander Corn
513b0bff1c Added some optimizations and backwards-compatibility for inv fetching 2016-12-13 00:46:02 -05:00
Alexander Corn
ab037b1931 Pass response body back to caller for inventory fetching 2016-12-13 00:45:38 -05:00
Alexander Corn
06df0ecd42 Handle currencies with new endpoint's data in CEconItem 2016-12-13 00:45:23 -05:00
João Silva
b789e13a70 Update inventory loading endpoint (#148)
* Update inventory loading endpoint

Should fix 429s :)

* Fix missing contextID parameter at CEconItem class

* Revert "Fix missing contextID parameter at CEconItem class"

This reverts commit 416321105b.

* Revert "Revert "Fix missing contextID parameter at CEconItem class""

This reverts commit 49ecd46954.

* Update #1
2016-12-13 00:23:02 -05:00
Alexander Corn
95a36b00c1 Remember time offset for purposes of acceptConfirmationForObject
Closes #147
2016-12-08 17:57:01 -05:00
Alexander Corn
41c87501c4 3.28.0 2016-10-28 19:24:05 -04:00
Alexander Corn
4f3ddcc787 Merge pull request #134 from Aareksio/confirmations
Emit `confirmationAccepted` after accepting the confirmation using `confirmationChecker`
2016-10-27 17:07:41 -04:00
Arkadiusz Sygulski
d10fa9983d Emit confirmationAccepted after accepting the confirmation 2016-10-27 22:30:58 +02:00
Arkadiusz Sygulski
666226731e Remove unused function: handleNewConfirmation 2016-10-27 22:18:31 +02:00
Alexander Corn
d096f47cac 3.27.2 2016-10-10 00:55:36 -04:00
Alexander Corn
a8eedb3067 Make it possible to skip json error check 2016-10-10 00:55:19 -04:00
Alexander Corn
ed12a54e81 3.27.1 2016-10-04 09:16:08 -04:00
Alexander Corn
eb341314ef Merge pull request #130 from Pavelas/master
Delete the profile URL cache after log in.
2016-10-04 09:15:03 -04:00
Pavelas
33cd9474ab Delete the profile URL cache after log in. 2016-10-04 10:26:20 +02:00
Alexander Corn
e8a43734be 3.27.0 2016-09-16 19:30:32 -04:00
Alexander Corn
8a27147f5c Added accountAlerts to getNotifications 2016-09-16 19:30:19 -04:00
Alexander Corn
d00d07f828 Added acceptConfirmationForObject convenience method 2016-09-16 19:03:49 -04:00
Alexander Corn
471c11d406 Merge pull request #128 from SzymonLisowiec/patch-1
Problem when search "Key:" in polish language.
2016-09-16 18:21:17 -04:00
Alexander Corn
dd3a9254aa 3.26.1 2016-09-12 11:22:29 -04:00
Alexander Corn
71a3893636 Fixed cookies not being set for non-community domains after logging in
Also fixed secure flag not working for cookies
2016-09-12 11:22:18 -04:00
Alexander Corn
5928c3b76a 3.26.0 2016-09-12 01:33:30 -04:00
Alexander Corn
1b60712b8b Updated getNotifications to use the new JSON endpoint 2016-09-12 01:33:11 -04:00
Alexander Corn
335a955abd Fixed cookies not being set 2016-09-12 01:24:39 -04:00
Alexander Corn
6d534df8ba Set cookies on all Steam domains, and properly flag as secure 2016-09-12 01:17:00 -04:00
Alexander Corn
9022a9a45e 3.25.0 2016-09-08 19:45:05 -04:00
Alexander Corn
ce4d7ae026 Pull trade offer ID directly from listing page 2016-09-08 19:23:15 -04:00
Kysune
b863377c87 Problem when search "Key:" in polish language. 2016-09-01 21:05:46 +02:00
Alexander Corn
20785ee92b 3.24.0 2016-08-31 22:20:49 -04:00
Alexander Corn
3afb393f58 Updated getInventoryHistory for new pagination scheme 2016-08-31 22:20:36 -04:00
Alexander Corn
79543187f0 Fixed timezones being incorrect in trade history entries 2016-08-31 22:20:24 -04:00
Alexander Corn
3139aec463 Reformat inventoryhistory.js 2016-08-31 21:48:17 -04:00
Alexander Corn
bb917af86e 3.23.3 2016-08-10 18:55:36 -04:00
Alexander Corn
17f51b9caa Fixed double callbacks on some json errors (fixes #123) 2016-08-10 18:55:23 -04:00
Alexander Corn
dd638df118 3.23.2 2016-08-09 15:45:32 -04:00
Alexander Corn
b937a05442 Make sure we actually get json back when we want it (fixes #122) 2016-08-09 15:45:21 -04:00
57 changed files with 4190 additions and 903 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

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'

55
.gitignore vendored
View File

@@ -1,3 +1,54 @@
node_modules/*
node_modules/
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>

15
.idea/codeStyles/Project.xml generated Normal file
View File

@@ -0,0 +1,15 @@
<component name="ProjectCodeStyleConfiguration">
<code_scheme name="Project" version="173">
<codeStyleSettings language="JSON">
<indentOptions>
<option name="USE_TAB_CHARACTER" value="true" />
</indentOptions>
</codeStyleSettings>
<codeStyleSettings language="JavaScript">
<indentOptions>
<option name="USE_TAB_CHARACTER" value="true" />
<option name="SMART_TABS" 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,11 @@
<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="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" />
</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>

1
.npmrc Normal file
View File

@@ -0,0 +1 @@
package-lock=false

View File

@@ -28,6 +28,7 @@ Submitting a pull request? Great! Thanks for contributing your time and code! Pl
- Tabs for indentation
- camelCase for variables and functions
- Opening braces on the same line as the if/for/while statement
- No omitting unnecessary braces
- etc.
- Please avoid breaking changes. If you make a breaking change that can be done in a backwards-compatible manner, I won't accept it.
- Please don't increment the version number in `package.json`. I'll do that myself when I publish it to npm.

View File

@@ -1,6 +1,6 @@
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
of this software and associated documentation files (the "Software"), to deal

View File

@@ -1,13 +1,11 @@
# Steam Community for Node.js
[![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)
[![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)
[![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)
This module provides an easy interface for the Steam Community website. This module can be used to simply login to steamcommunity.com for use with other libraries, or to interact with steamcommunity.com.
It supports Steam Guard and CAPTCHAs.
This module provides an easy interface for the Steam Community website. This module can be used to simply login to
steamcommunity.com for use with other libraries, or to interact with steamcommunity.com.
**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/).

View File

@@ -1,17 +1,34 @@
var SteamCommunity = require('../index.js');
module.exports = CConfirmation;
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.creator = data.creator.toString();
this.key = data.key;
this.title = data.title;
this.receiving = data.receiving;
this.sending = data.sending;
this.time = data.time;
this.timestamp = data.timestamp;
this.icon = data.icon;
this.offerID = this.type == SteamCommunity.ConfirmationType.Trade ? this.creator : null;
}
CConfirmation.prototype.getOfferID = function(time, key, callback) {
if (this.type && this.creator) {
if (this.type != SteamCommunity.ConfirmationType.Trade) {
callback(new Error('Not a trade confirmation'));
return;
}
callback(null, this.creator);
return;
}
this._community.getConfirmationOfferID(this.id, time, key, callback);
};

View File

@@ -1,31 +1,40 @@
module.exports = CEconItem;
function CEconItem(item, descriptions, contextID) {
function CEconItem(item, description, contextID, assetProperties) {
var thing;
for(thing in item) {
if(item.hasOwnProperty(thing)) {
for (thing in item) {
if (item.hasOwnProperty(thing)) {
this[thing] = item[thing];
}
}
this.assetid = this.id = (this.id || this.assetid);
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
if (isCurrency) {
this.currencyid = this.id = (this.id || this.currencyid);
} else {
this.assetid = this.id = (this.id || this.assetid);
}
this.instanceid = this.instanceid || '0';
this.amount = parseInt(this.amount, 10);
this.contextid = this.contextid || contextID.toString();
// Merge the description
if(descriptions) {
var description = descriptions[this.classid + '_' + this.instanceid];
if(description) {
for(thing in description) {
if(description.hasOwnProperty(thing)) {
this[thing] = description[thing];
}
if (description) {
// Is this a listing of descriptions?
if (description[this.classid + '_' + this.instanceid]) {
description = description[this.classid + '_' + this.instanceid];
}
for (thing in description) {
if (description.hasOwnProperty(thing) && !this.hasOwnProperty(thing)) {
this[thing] = description[thing];
}
}
}
this.is_currency = !!this.is_currency;
this.is_currency = isCurrency;
this.tradable = !!this.tradable;
this.marketable = !!this.marketable;
this.commodity = !!this.commodity;
@@ -34,9 +43,59 @@ function CEconItem(item, descriptions, contextID) {
this.fraudwarnings = this.fraudwarnings || [];
this.descriptions = this.descriptions || [];
if(this.owner && JSON.stringify(this.owner) == '{}') {
if (this.owner && JSON.stringify(this.owner) == '{}') {
this.owner = null;
}
// Restore old property names of tags
if (this.tags) {
this.tags = this.tags.map(function(tag) {
return {
"internal_name": tag.internal_name,
"name": tag.localized_tag_name || tag.name,
"category": tag.category,
"color": tag.color || "",
"category_name": tag.localized_category_name || tag.category_name
};
});
}
// Restore market_fee_app, if applicable
var match;
if (this.appid == 753 && this.contextid == 6 && this.market_hash_name && (match = this.market_hash_name.match(/^(\d+)\-/))) {
this.market_fee_app = parseInt(match[1], 10);
}
// Restore cache_expiration, if we can (for CS:GO items)
if (this.appid == 730 && this.contextid == 2 && this.owner_descriptions) {
let description = this.owner_descriptions.find(d => d.value && d.value.indexOf('Tradable/Marketable After ') == 0);
if (description) {
let date = new Date(description.value.substring(26).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 = [];
}
// Assign asset_properties if provided
if (assetProperties) {
this.asset_properties = assetProperties;
}
// 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() {
@@ -52,12 +111,12 @@ CEconItem.prototype.getLargeImageURL = function() {
};
CEconItem.prototype.getTag = function(category) {
if(!this.tags) {
if (!this.tags) {
return null;
}
for(var i = 0; i < this.tags.length; i++) {
if(this.tags[i].category == category) {
for (var i = 0; i < this.tags.length; i++) {
if (this.tags[i].category == category) {
return this.tags[i];
}
}

View File

@@ -12,7 +12,7 @@ SteamCommunity.prototype.getMarketItem = function(appid, hashName, currency, cal
callback(err);
return;
}
var $ = Cheerio.load(body);
if($('.market_listing_table_message') && $('.market_listing_table_message').text().trim() == 'There are no listings for this item.') {
callback(new Error("There are no listings for this item."));
@@ -35,7 +35,7 @@ function CMarketItem(appid, hashName, community, body, $) {
this._hashName = hashName;
this._community = community;
this._$ = $;
this._country = "US";
var match = body.match(/var g_strCountryCode = "([^"]+)";/);
if(match) {
@@ -47,14 +47,14 @@ function CMarketItem(appid, hashName, community, body, $) {
if(match) {
this._language = match[1];
}
this.commodity = false;
var match = body.match(/Market_LoadOrderSpread\(\s*(\d+)\s*\);/);
match = body.match(/Market_LoadOrderSpread\(\s*(\d+)\s*\);/);
if(match) {
this.commodity = true;
this.commodityID = parseInt(match[1], 10);
}
this.medianSalePrices = null;
match = body.match(/var line1=([^;]+);/);
if(match) {
@@ -78,7 +78,8 @@ function CMarketItem(appid, hashName, community, body, $) {
if (match) {
try {
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]];
} catch (e) {
// ignore
@@ -112,29 +113,29 @@ CMarketItem.prototype.updatePriceForCommodity = function(currency, callback) {
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);

View File

@@ -3,26 +3,26 @@ var Cheerio = require('cheerio');
SteamCommunity.prototype.marketSearch = function(options, callback) {
var qs = {};
if(typeof options === 'string') {
qs.query = options;
} else {
qs.query = options.query || '';
qs.appid = options.appid;
qs.search_descriptions = options.searchDescriptions ? 1 : 0;
if(qs.appid) {
for(var i in options) {
if(['query', 'appid', 'searchDescriptions'].indexOf(i) != -1) {
continue;
}
// This is a tag
qs['category_' + qs.appid + '_' + i + '[]'] = 'tag_' + options[i];
}
}
}
qs.start = 0;
qs.count = 100;
qs.sort_column = 'price';
@@ -80,10 +80,10 @@ SteamCommunity.prototype.marketSearch = function(options, callback) {
function CMarketSearchResult(row) {
var match = row.attr('href').match(/\/market\/listings\/(\d+)\/([^\?\/]+)/);
this.appid = parseInt(match[1], 10);
this.market_hash_name = decodeURIComponent(match[2]);
this.image = row.find('.market_listing_item_img').attr('src').match(/^https?:\/\/[^\/]+\/economy\/image\/[^\/]+\//)[0];
this.image = ((row.find('.market_listing_item_img').attr('src') || "").match(/^https?:\/\/[^\/]+\/economy\/image\/[^\/]+\//) || [])[0];
this.price = parseInt(row.find('.market_listing_their_price .market_table_value span.normal_price').text().replace(/[^\d]+/g, ''), 10);
this.quantity = parseInt(row.find('.market_listing_num_listings_qty').text().replace(/[^\d]+/g, ''), 10);
}

View File

@@ -78,8 +78,8 @@ CSteamGroup.prototype.getAllAnnouncements = function(time, callback) {
this._community.getAllGroupAnnouncements(this.steamID, time, callback);
};
CSteamGroup.prototype.postAnnouncement = function(headline, content, callback) {
this._community.postGroupAnnouncement(this.steamID, headline, content, callback);
CSteamGroup.prototype.postAnnouncement = function(headline, content, hidden, callback) {
this._community.postGroupAnnouncement(this.steamID, headline, content, hidden, callback);
};
CSteamGroup.prototype.editAnnouncement = function(annoucementID, headline, content, callback) {
@@ -113,3 +113,43 @@ CSteamGroup.prototype.kick = function(steamID, callback) {
CSteamGroup.prototype.getHistory = function(page, callback) {
this._community.getGroupHistory(this.steamID, page, callback);
};
CSteamGroup.prototype.getAllComments = function(from, count, callback) {
this._community.getAllGroupComments(this.steamID, from, count, callback);
};
CSteamGroup.prototype.deleteComment = function(cid, callback) {
this._community.deleteGroupComment(this.steamID, cid, callback);
};
CSteamGroup.prototype.comment = function(message, callback) {
this._community.postGroupComment(this.steamID, message, callback);
};
/**
* Get requests to join this restricted group.
* @param {function} callback - First argument is null/Error, second is array of SteamID objects
*/
CSteamGroup.prototype.getJoinRequests = function(callback) {
this._community.getGroupJoinRequests(this.steamID, callback);
};
/**
* Respond to one or more join requests to this restricted group.
* @param {SteamID|string|SteamID[]|string[]} steamIDs - The SteamIDs of the users you want to approve or deny membership for (or a single value)
* @param {boolean} approve - True to put them in the group, false to deny their membership
* @param {function} callback - Takes only an Error object/null as the first argument
*/
CSteamGroup.prototype.respondToJoinRequests = function(steamIDs, approve, callback) {
this._community.respondToGroupJoinRequests(this.steamID, steamIDs, approve, callback);
};
/**
* Respond to *ALL* pending group-join requests for this group.
* @param {boolean} approve - True to allow everyone who requested into the group, false to not
* @param {function} callback - Takes only an Error object/null as the first argument
*/
CSteamGroup.prototype.respondToAllJoinRequests = function(approve, callback) {
this._community.respondToAllGroupJoinRequests(this.steamID, approve, callback);
};

221
classes/CSteamSharedFile.js Normal file
View File

@@ -0,0 +1,221 @@
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}`, (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"];
Helpers.resolveVanityURL(ownerHref, (err, data) => { // This request takes <1 sec
if (err) {
callback(err);
return;
}
sharedfile.owner = new SteamID(data.steamID);
// Make callback when ID was resolved as otherwise owner will always be null
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

@@ -7,29 +7,29 @@ SteamCommunity.prototype.getSteamUser = function(id, callback) {
if(typeof id !== 'string' && !Helpers.isSteamID(id)) {
throw new Error("id parameter should be a user URL string or a SteamID object");
}
if(typeof id === 'object' && (id.universe != SteamID.Universe.PUBLIC || id.type != SteamID.Type.INDIVIDUAL)) {
throw new Error("SteamID must stand for an individual account in the public universe");
}
var self = this;
this.httpRequest("http://steamcommunity.com/" + (typeof id === 'string' ? "id/" + id : "profiles/" + id.toString()) + "/?xml=1", function(err, response, body) {
this.httpRequest("https://steamcommunity.com/" + (typeof id === 'string' ? "id/" + id : "profiles/" + id.toString()) + "/?xml=1", function(err, response, body) {
if (err) {
callback(err);
return;
}
xml2js.parseString(body, function(err, result) {
if(err || (!result.response && !result.profile)) {
callback(err || new Error("No valid response"));
return;
}
if(result.response && result.response.error && result.response.error.length) {
callback(new Error(result.response.error[0]));
return;
}
// Try and find custom URL from redirect
var customurl = null;
if(response.request.redirects && response.request.redirects.length) {
@@ -43,7 +43,7 @@ SteamCommunity.prototype.getSteamUser = function(id, callback) {
callback(new Error("No valid response"));
return;
}
callback(null, new CSteamUser(self, result.profile, customurl));
});
}, "steamcommunity");
@@ -51,7 +51,7 @@ SteamCommunity.prototype.getSteamUser = function(id, callback) {
function CSteamUser(community, userData, customurl) {
this._community = community;
this.steamID = new SteamID(userData.steamID64[0]);
this.name = processItem('steamID');
this.onlineState = processItem('onlineState');
@@ -67,9 +67,15 @@ function CSteamUser(community, userData, customurl) {
this.tradeBanState = processItem('tradeBanState', 'None');
this.isLimitedAccount = processItem('isLimitedAccount') == 1;
this.customURL = processItem('customURL', customurl);
if(this.visibilityState == 3) {
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.realName = processItem('realname');
this.summary = processItem('summary');
@@ -79,19 +85,19 @@ function CSteamUser(community, userData, customurl) {
this.realName = null;
this.summary = null;
}
// Maybe handle mostPlayedGames?
this.groups = null;
this.primaryGroup = null;
var self = this;
if(userData.groups && userData.groups[0] && userData.groups[0].group) {
this.groups = userData.groups[0].group.map(function(group) {
if(group['$'] && group['$'].isPrimary === "1") {
self.primaryGroup = new SteamID(group.groupID64[0]);
}
return new SteamID(group.groupID64[0]);
});
}
@@ -108,7 +114,9 @@ function CSteamUser(community, userData, customurl) {
CSteamUser.getAvatarURL = function(hash, size, protocol) {
size = size || '';
protocol = protocol || 'http://';
hash = hash || "72f78b4c8cc1f62323f8a33f6d53e27db57c2252"; // The default "?" avatar
var url = protocol + "steamcdn-a.akamaihd.net/steamcommunity/public/images/avatars/" + hash.substring(0, 2) + "/" + hash;
if(size == 'full' || size == 'medium') {
return url + "_" + size + ".jpg";
@@ -146,10 +154,26 @@ CSteamUser.prototype.comment = function(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) {
this._community.inviteUserToGroup(this.steamID, groupID, callback);
};
CSteamUser.prototype.follow = function(callback) {
this._community.followUser(this.steamID, callback);
};
CSteamUser.prototype.unfollow = function(callback) {
this._community.unfollowUser(this.steamID, callback);
};
CSteamUser.prototype.getAliases = function(callback) {
this._community.getUserAliases(this.steamID, callback);
};
@@ -158,6 +182,44 @@ CSteamUser.prototype.getInventoryContexts = function(callback) {
this._community.getUserInventoryContexts(this.steamID, callback);
};
/**
* Get the contents of a user's inventory context.
* @deprecated Use CSteamUser#getInventoryContents instead
* @param {int} appID - The Steam application ID of the game for which you want an inventory
* @param {int} contextID - The ID of the "context" within the game you want to retrieve
* @param {boolean} tradableOnly - true to get only tradable items and currencies
* @param callback
*/
CSteamUser.prototype.getInventory = function(appID, contextID, tradableOnly, callback) {
this._community.getUserInventory(this.steamID, appID, contextID, tradableOnly, callback);
};
/**
* Get the contents of a user's inventory context.
* @param {int} appID - The Steam application ID of the game for which you want an inventory
* @param {int} contextID - The ID of the "context" within the game you want to retrieve
* @param {boolean} tradableOnly - true to get only tradable items and currencies
* @param {string} [language] - The language of item descriptions to return. Omit for default (which may either be English or your account's chosen language)
* @param callback
*/
CSteamUser.prototype.getInventoryContents = function(appID, contextID, tradableOnly, language, callback) {
this._community.getUserInventoryContents(this.steamID, appID, contextID, tradableOnly, language, callback);
};
/**
* Get the background URL of user's profile.
* @param {function} callback
*/
CSteamUser.prototype.getProfileBackground = function(callback) {
this._community.getUserProfileBackground(this.steamID, callback);
};
/**
* Upload an image to Steam and send it to the target user over chat.
* @param {Buffer} imageContentsBuffer - The image contents, as a Buffer
* @param {{spoiler?: boolean}} [options]
* @param {function} callback
*/
CSteamUser.prototype.sendImage = function(imageContentsBuffer, options, callback) {
this._community.sendImageToUser(this.steamID, imageContentsBuffer, options, callback);
};

View File

@@ -1,44 +1,26 @@
var SteamCommunity = require('../index.js');
var SteamID = require('steamid');
SteamCommunity.ChatState = {
"Offline": 0,
"LoggingOn": 1,
"LogOnFailed": 2,
"LoggedOn": 3
};
SteamCommunity.PersonaState = {
"Offline": 0,
"Online": 1,
"Busy": 2,
"Away": 3,
"Snooze": 4,
"LookingToTrade": 5,
"LookingToPlay": 6,
"Max": 7
};
SteamCommunity.PersonaStateFlag = {
"HasRichPresence": 1,
"InJoinableGame": 2,
"OnlineUsingWeb": 256,
"OnlineUsingMobile": 512,
"OnlineUsingBigPicture": 1024
};
SteamCommunity.ChatState = require('../resources/EChatState.js');
SteamCommunity.PersonaState = require('../resources/EPersonaState.js');
SteamCommunity.PersonaStateFlag = require('../resources/EPersonaStateFlag.js');
/**
* @deprecated No support for new Steam chat. Use steam-user instead.
* @param {int} interval
* @param {string} uiMode
*/
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) {
@@ -55,7 +37,7 @@ SteamCommunity.prototype.chatLogon = function(interval, uiMode) {
self.emit('debug', "Cannot get oauth token: " + err.message);
return;
}
self.httpRequestPost({
"uri": "https://api.steampowered.com/ISteamWebUserPresenceOAuth/Logon/v1",
"form": {
@@ -71,7 +53,7 @@ SteamCommunity.prototype.chatLogon = function(interval, uiMode) {
setTimeout(self.chatLogon.bind(self), 5000);
return;
}
if(body.error != 'OK') {
self.chatState = SteamCommunity.ChatState.LogOnFailed;
self.emit('chatLogOnFailed', new Error(body.error), false);
@@ -79,7 +61,7 @@ SteamCommunity.prototype.chatLogon = function(interval, uiMode) {
setTimeout(self.chatLogon.bind(self), 5000);
return;
}
self._chat = {
"umqid": body.umqid,
"message": body.message,
@@ -87,9 +69,9 @@ SteamCommunity.prototype.chatLogon = function(interval, uiMode) {
"interval": interval,
"uiMode": uiMode
};
self.chatFriends = {};
self.chatState = SteamCommunity.ChatState.LoggedOn;
self.emit('chatLoggedOn');
self._chatPoll();
@@ -97,20 +79,27 @@ SteamCommunity.prototype.chatLogon = function(interval, uiMode) {
});
};
/**
* @deprecated No support for new Steam chat. Use steam-user instead.
* @param {string|SteamID} recipient
* @param {string} text
* @param {string} [type]
* @param {function} [callback]
*/
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;
@@ -128,12 +117,12 @@ SteamCommunity.prototype.chatMessage = function(recipient, text, type, callback)
if(!callback) {
return;
}
if (err) {
callback(err);
return;
}
if(body.error != 'OK') {
callback(new Error(body.error));
} else {
@@ -142,6 +131,9 @@ SteamCommunity.prototype.chatMessage = function(recipient, text, type, callback)
}, "steamcommunity");
};
/**
* @deprecated No support for new Steam chat. Use steam-user instead.
*/
SteamCommunity.prototype.chatLogoff = function() {
var self = this;
this.httpRequestPost({
@@ -164,9 +156,12 @@ SteamCommunity.prototype.chatLogoff = function() {
}, "steamcommunity");
};
/**
* @private
*/
SteamCommunity.prototype._chatPoll = function() {
this.emit('debug', 'Doing chat poll');
var self = this;
this.httpRequestPost({
"uri": "https://api.steampowered.com/ISteamWebUserPresenceOAuth/Poll/v1",
@@ -181,12 +176,12 @@ SteamCommunity.prototype._chatPoll = function() {
},
"json": true
}, function(err, response, body) {
if(self.chatState == SteamCommunity.ChatState.Offline) {
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") {
@@ -195,7 +190,7 @@ SteamCommunity.prototype._chatPoll = function() {
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") {
@@ -204,29 +199,29 @@ SteamCommunity.prototype._chatPoll = function() {
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);
}
@@ -234,6 +229,9 @@ SteamCommunity.prototype._chatPoll = function() {
}, "steamcommunity");
};
/**
* @private
*/
SteamCommunity.prototype._relogWebChat = function() {
this.emit('debug', "Relogging web chat");
clearTimeout(this._chat.timer);
@@ -241,13 +239,25 @@ SteamCommunity.prototype._relogWebChat = function() {
this.chatLogon(this._chat.interval, this._chat.uiMode);
};
/**
* @param {SteamID} steamID
* @private
*/
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() {
@@ -255,7 +265,7 @@ SteamCommunity.prototype._chatUpdatePersona = function(steamID) {
}, 2000);
return;
}
var persona = {
"steamID": steamID,
"personaName": body.m_strName,
@@ -270,4 +280,4 @@ SteamCommunity.prototype._chatUpdatePersona = function(steamID) {
self.emit('chatPersonaState', steamID, persona);
self.chatFriends[steamID.getSteamID64()] = persona;
}, "steamcommunity");
};
};

View File

@@ -4,61 +4,55 @@ var SteamTotp = require('steam-totp');
var Async = require('async');
var CConfirmation = require('../classes/CConfirmation.js');
var EConfirmationType = require('../resources/EConfirmationType.js');
/**
* Get a list of your account's currently outstanding confirmations.
* @param {int} time - The unix timestamp with which the following key was generated
* @param {string} key - The confirmation key that was generated using the preceeding time and the tag "conf" (this key can be reused)
* @param {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
*/
SteamCommunity.prototype.getConfirmations = function(time, key, callback) {
var self = this;
request(this, "conf", key, time, "conf", null, false, function(err, body) {
if(err) {
if (err.message == "Invalid protocol: steammobile:") {
err.message = "Not Logged In";
self._notifySessionExpired(err);
}
// Ugly hack to maintain backward compatibility
var tag = 'conf';
if (typeof key == 'object') {
tag = key.tag;
key = key.key;
}
// The official Steam app uses the tag 'list', but 'conf' still works so let's use that for backward compatibility.
request(this, 'getlist', key, time, tag, null, true, function(err, body) {
if (err) {
callback(err);
return;
}
var $ = Cheerio.load(body);
var empty = $('#mobileconf_empty');
if(empty.length > 0) {
if(!$(empty).hasClass('mobileconf_done')) {
// An error occurred
callback(new Error(empty.find('div:nth-of-type(2)').text()));
} else {
callback(null, []);
if (!body.success) {
if (body.needauth) {
var err = new Error('Not Logged In');
self._notifySessionExpired(err);
callback(err);
return;
}
callback(new Error(body.message || body.detail || 'Failed to get confirmation list'));
return;
}
// 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'),
"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')
}));
});
var confs = (body.conf || []).map(conf => new CConfirmation(self, {
id: conf.id,
type: conf.type,
creator: conf.creator_id,
key: conf.nonce,
title: `${conf.type_name || 'Confirm'} - ${conf.headline || ''}`,
receiving: conf.type == EConfirmationType.Trade ? ((conf.summary || [])[1] || '') : '',
sending: (conf.summary || [])[0] || '',
time: (new Date(conf.creation_time * 1000)).toISOString(), // for backward compatibility
timestamp: new Date(conf.creation_time * 1000),
icon: conf.icon || ''
}));
callback(null, confs);
});
@@ -74,22 +68,23 @@ SteamCommunity.prototype.getConfirmations = function(time, key, callback) {
* Get the trade offer ID associated with a particular confirmation
* @param {int} confID - The ID of the confirmation in question
* @param {int} time - The unix timestamp with which the following key was generated
* @param {string} key - The confirmation key that was generated using the preceeding time and the tag "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
*/
SteamCommunity.prototype.getConfirmationOfferID = function(confID, time, key, callback) {
request(this, "details/" + confID, key, time, "details", null, true, function(err, body) {
if(err) {
// The official Steam app uses the tag 'detail', but 'details' still works so let's use that for backward compatibility
request(this, 'detailspage/' + confID, key, time, 'details', null, false, function(err, body) {
if (err) {
callback(err);
return;
}
if(!body.success) {
if (typeof body != 'string') {
callback(new Error("Cannot load confirmation details"));
return;
}
var $ = Cheerio.load(body.html);
var $ = Cheerio.load(body);
var offer = $('.tradeoffer');
if(offer.length < 1) {
callback(null, null);
@@ -111,39 +106,120 @@ SteamCommunity.prototype.getConfirmationOfferID = function(confID, time, key, ca
* @param {int|int[]} confID - The ID of the confirmation in question, or an array of confirmation IDs
* @param {string|string[]} confKey - The confirmation key associated with the confirmation in question (or an array of them) (not a TOTP key, the `key` property of CConfirmation)
* @param {int} time - The unix timestamp with which the following key was generated
* @param {string} key - The confirmation key that was generated using the preceeding 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 {SteamCommunity~genericErrorCallback} callback - Called when the request is complete
*/
SteamCommunity.prototype.respondToConfirmation = function(confID, confKey, time, key, accept, callback) {
request(this, (confID instanceof Array) ? "multiajaxop" : "ajaxop", key, time, accept ? "allow" : "cancel", {
"op": accept ? "allow" : "cancel",
"cid": confID,
"ck": confKey
// Ugly hack to maintain backward compatibility
var tag = accept ? 'allow' : 'cancel';
if (typeof key == 'object') {
tag = key.tag;
key = key.key;
}
// The official app uses tags reject/accept, but cancel/allow still works so use these for backward compatibility
request(this, (confID instanceof Array) ? 'multiajaxop' : 'ajaxop', key, time, tag, {
op: accept ? 'allow' : 'cancel',
cid: confID,
ck: confKey
}, true, function(err, body) {
if(!callback) {
if (!callback) {
return;
}
if(err) {
if (err) {
callback(err);
return;
}
if(body.success) {
if (body.success) {
callback(null);
return;
}
if(body.message) {
if (body.message) {
callback(new Error(body.message));
return;
}
callback(new Error("Could not act on confirmation"));
callback(new Error('Could not act on confirmation'));
});
};
/**
* Accept a confirmation for a given object (trade offer or market listing) automatically.
* @param {string} identitySecret
* @param {number|string} objectID
* @param {SteamCommunity~genericErrorCallback} callback
*/
SteamCommunity.prototype.acceptConfirmationForObject = function(identitySecret, objectID, callback) {
var self = this;
this._usedConfTimes = this._usedConfTimes || [];
if (typeof this._timeOffset !== 'undefined') {
// time offset is already known and saved
doConfirmation();
} else {
SteamTotp.getTimeOffset(function(err, offset) {
if (err) {
callback(err);
return;
}
self._timeOffset = offset;
doConfirmation();
setTimeout(function() {
// Delete the saved time offset after 12 hours because why not
delete self._timeOffset;
}, 1000 * 60 * 60 * 12).unref();
});
}
function doConfirmation() {
var offset = self._timeOffset;
var time = SteamTotp.time(offset);
var confKey = SteamTotp.getConfirmationKey(identitySecret, time, 'list');
self.getConfirmations(time, {tag: 'list', key: confKey}, function(err, confs) {
if (err) {
callback(err);
return;
}
var conf = confs.filter(function(conf) { return conf.creator == objectID; });
if (conf.length == 0) {
callback(new Error('Could not find confirmation for object ' + objectID));
return;
}
conf = conf[0];
// make sure we don't reuse the same time
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
}
confKey = SteamTotp.getConfirmationKey(identitySecret, time, 'accept');
conf.respond(time, {tag: 'accept', key: confKey}, true, callback);
});
}
};
/**
* Send a single request to Steam to accept all outstanding confirmations (after loading the list). If one fails, the
* entire request will fail and there will be no way to know which failed without loading the list again.
* @param {number} time
* @param {string} confKey
* @param {string} allowKey
* @param {function} callback
*/
SteamCommunity.prototype.acceptAllConfirmations = function(time, confKey, allowKey, callback) {
var self = this;
@@ -170,21 +246,25 @@ SteamCommunity.prototype.acceptAllConfirmations = function(time, confKey, allowK
};
function request(community, url, key, time, tag, params, json, callback) {
if (!community.steamID) {
throw new Error('Must be logged in before trying to do anything with confirmations');
}
params = params || {};
params.p = SteamTotp.getDeviceID(community.steamID);
params.a = community.steamID.getSteamID64();
params.k = key;
params.t = time;
params.m = "android";
params.m = 'react';
params.tag = tag;
var req = {
"method": url == 'multiajaxop' ? 'POST' : 'GET',
"uri": "https://steamcommunity.com/mobileconf/" + url,
"json": !!json
method: url == 'multiajaxop' ? 'POST' : 'GET',
uri: 'https://steamcommunity.com/mobileconf/' + url,
json: !!json
};
if (req.method == "GET") {
if (req.method == 'GET') {
req.qs = params;
} else {
req.form = params;
@@ -197,7 +277,7 @@ function request(community, url, key, time, tag, params, json, callback) {
}
callback(null, body);
}, "steamcommunity");
}, 'steamcommunity');
}
// Confirmation checker
@@ -256,8 +336,9 @@ SteamCommunity.prototype.checkConfirmations = function() {
// 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() {
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
});
@@ -294,24 +375,13 @@ SteamCommunity.prototype.checkConfirmations = function() {
return; // No new ones
}
// We have new confirmations! Grab a key to get details.
self._confirmationCheckerGetKey('details', function(err, key) {
newOnes.forEach(function(conf) {
self._knownConfirmations[conf.id] = conf; // Add it to our list of known confirmations
if(err) {
self._confirmationQueue.push(conf);
} else {
// Get its offer ID, if we can
conf.getOfferID(key.time, key.key, function(err, offerID) {
conf.offerID = offerID ? offerID : null;
self._confirmationQueue.push(conf);
});
}
});
resetTimer();
// 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();
});
});
@@ -320,30 +390,24 @@ SteamCommunity.prototype.checkConfirmations = function() {
self._confirmationTimer = setTimeout(self.checkConfirmations.bind(self), self._confirmationPollInterval);
}
}
function handleNewConfirmation(conf, handleNumber) {
self._knownConfirmations[conf.id] = conf;
// Delay them by 1 second per new confirmation that we see, so that keys won't be the same.
setTimeout(function() {
if(self._identitySecret) {
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 (err) {
self.emit('debug', "Can't accept confirmation #" + conf.id + ": " + err.message);
}
// We'll just retry next time we poll
delete self._knownConfirmations[conf.id];
});
} else {
self.emit('newConfirmation', conf);
}
}, handleNumber * 1000);
}
};
SteamCommunity.prototype.acknowledgeTradeProtection = function(callback) {
this.httpRequestPost({
uri: 'https://steamcommunity.com//trade/new/acknowledge',
form: {
sessionid: this.getSessionID(),
message: 1
}
}, (err, res, body) => {
if (err) {
return callback && callback(err);
}
callback && callback(null);
}, 'steamcommunity');
}
SteamCommunity.prototype._confirmationCheckerGetKey = function(tag, callback) {
if(this._identitySecret) {
if(tag == 'details') {

View File

@@ -2,6 +2,8 @@ var SteamCommunity = require('../index.js');
var SteamID = require('steamid');
var xml2js = require('xml2js');
var Cheerio = require('cheerio');
var Helpers = require('./helpers.js');
var EResult = SteamCommunity.EResult;
SteamCommunity.prototype.getGroupMembers = function(gid, callback, members, link, addresses, addressIdx) {
members = members || [];
@@ -9,17 +11,17 @@ SteamCommunity.prototype.getGroupMembers = function(gid, callback, members, link
if (!link) {
if (typeof gid !== 'string') {
// 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 {
try {
var sid = new SteamID(gid);
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 {
throw new Error("Doesn't particularly matter what this message is");
}
} catch (e) {
link = "http://steamcommunity.com/groups/" + gid + "/memberslistxml/?xml=1";
link = "https://steamcommunity.com/groups/" + gid + "/memberslistxml/?xml=1";
}
}
}
@@ -142,7 +144,7 @@ SteamCommunity.prototype.getAllGroupAnnouncements = function(gid, time, callback
headline: announcement.title[0],
content: announcement.description[0],
date: new Date(announcement.pubDate[0]),
author: announcement.author[0],
author: (typeof announcement.author === 'undefined') ? null : announcement.author[0],
aid: splitLink[splitLink.length - 1]
}
}).filter(function(announcement) {
@@ -152,24 +154,35 @@ SteamCommunity.prototype.getAllGroupAnnouncements = function(gid, time, callback
return callback(null, announcements);
});
}, "steamcommunity");
}
};
SteamCommunity.prototype.postGroupAnnouncement = function(gid, headline, content, callback) {
SteamCommunity.prototype.postGroupAnnouncement = function(gid, headline, content, hidden, callback) {
if(typeof gid === 'string') {
gid = new SteamID(gid);
}
if(typeof hidden === 'function') {
callback = hidden;
hidden = false;
}
var self = this;
var 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({
"uri": "https://steamcommunity.com/gid/" + gid.getSteamID64() + "/announcements",
"form": {
"sessionID": this.getSessionID(),
"action": "post",
"headline": headline,
"body": content,
"languages[0][headline]": headline,
"languages[0][body]": content
}
form
}, function(err, response, body) {
if(!callback) {
return;
@@ -493,7 +506,7 @@ SteamCommunity.prototype.getGroupHistory = function(gid, page, callback) {
sid.universe = SteamID.Universe.PUBLIC;
sid.type = SteamID.Type.INDIVIDUAL;
sid.instance = SteamID.Instance.DESKTOP;
sid.accountid = $(users[0]).data('miniprofile');
sid.accountid = $(users[1]).data('miniprofile');
data.actor = sid;
}
@@ -517,3 +530,269 @@ SteamCommunity.prototype.getGroupHistory = function(gid, page, callback) {
callback(null, output);
}, "steamcommunity");
};
SteamCommunity.prototype.getAllGroupComments = function(gid, from, count, callback) {
if (typeof gid === 'string') {
gid = new SteamID(gid);
}
var options = {
"uri": "https://steamcommunity.com/comment/Clan/render/" + gid.getSteamID64() + "/-1/",
"form": {
"start": from,
"count": count
}
};
var self = this;
this.httpRequestPost(options, function(err, response, body) {
if (err) {
callback(err);
return;
}
var comments = [];
var $ = Cheerio.load(JSON.parse(body).comments_html);
$(".commentthread_comment_content").each(function () {
var comment = {};
var cachedSelector;
var $selector = $(this).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 = $(this).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();
}
var options = {
"uri": "https://steamcommunity.com/comment/Clan/delete/" + gid.getSteamID64() + "/-1/",
"form": {
"sessionid": this.getSessionID(),
"gidcomment": cid
}
};
var self = this;
this.httpRequestPost(options, function(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);
}
var options = {
"uri": "https://steamcommunity.com/comment/Clan/post/" + gid.getSteamID64() + "/-1/",
"form": {
"comment": message,
"count": 6,
"sessionid": this.getSessionID()
}
};
var self = this;
this.httpRequestPost(options, function(err, response, body) {
if(!callback) {
return;
}
callback(err || null);
}, "steamcommunity");
};
/**
* Get requests to join a restricted group.
* @param {SteamID|string} gid - The SteamID of the group you want to manage
* @param {function} callback - First argument is null/Error, second is array of SteamID objects
*/
SteamCommunity.prototype.getGroupJoinRequests = function(gid, callback) {
if (typeof gid === 'string') {
gid = new SteamID(gid);
}
this.httpRequestGet("https://steamcommunity.com/gid/" + gid.getSteamID64() + "/joinRequestsManage", (err, res, body) => {
if (!body) {
callback(new Error("Malformed response"));
return;
}
var matches = body.match(/JoinRequests_ApproveDenyUser\(\W*['"](\d+)['"],\W0\W\)/g);
if (!matches) {
// no pending requests
callback(null, []);
return;
}
var requests = [];
for (var i = 0; i < matches.length; i++) {
requests.push(new SteamID("[U:1:" + matches[i].match(/JoinRequests_ApproveDenyUser\(\W*['"](\d+)['"],\W0\W\)/)[1] + "]"));
}
callback(null, requests);
}, "steamcommunity");
};
/**
* Respond to one or more join requests to a restricted group.
* @param {SteamID|string} gid - The SteamID of the group you want to manage
* @param {SteamID|string|SteamID[]|string[]} steamIDs - The SteamIDs of the users you want to approve or deny membership for (or a single value)
* @param {boolean} approve - True to put them in the group, false to deny their membership
* @param {function} callback - Takes only an Error object/null as the first argument
*/
SteamCommunity.prototype.respondToGroupJoinRequests = function(gid, steamIDs, approve, callback) {
if (typeof gid === 'string') {
gid = new SteamID(gid);
}
var rgAccounts = (!Array.isArray(steamIDs) ? [steamIDs] : steamIDs).map(sid => sid.toString());
this.httpRequestPost({
"uri": "https://steamcommunity.com/gid/" + gid.getSteamID64() + "/joinRequestsManage",
"form": {
"rgAccounts": rgAccounts,
"bapprove": approve ? "1" : "0",
"json": "1",
"sessionID": this.getSessionID()
},
"json": true
}, (err, res, body) => {
if (!callback) {
return;
}
if (body != EResult.OK) {
var err = new Error(EResult[body] || ("Error " + body));
err.eresult = body;
callback(err);
} else {
callback(null);
}
}, "steamcommunity");
};
/**
* Respond to *ALL* pending group-join requests for a particular group.
* @param {SteamID|string} gid - The SteamID of the group you want to manage
* @param {boolean} approve - True to allow everyone who requested into the group, false to not
* @param {function} callback - Takes only an Error object/null as the first argument
*/
SteamCommunity.prototype.respondToAllGroupJoinRequests = function(gid, approve, callback) {
if (typeof gid === 'string') {
gid = new SteamID(gid);
}
this.httpRequestPost({
"uri": "https://steamcommunity.com/gid/" + gid.getSteamID64() + "/joinRequestsManage",
"form": {
"bapprove": approve ? "1" : "0",
"json": "1",
"action": "bulkrespond",
"sessionID": this.getSessionID()
},
"json": true
}, (err, res, body) => {
if (!callback) {
return;
}
if (body != EResult.OK) {
var err = new Error(EResult[body] || ("Error " + body));
err.eresult = body;
callback(err);
} else {
callback(null);
}
}, "steamcommunity");
};
/**
* Follows a curator page
* @param {string|number} curatorId - ID of the curator (not a SteamID)
* @param {function} callback - Takes only an Error object/null as the first argument
*/
SteamCommunity.prototype.followCurator = function(curatorId, callback) {
this.httpRequestPost({
"uri": "https://store.steampowered.com/curators/ajaxfollow",
"form": {
"clanid": curatorId,
"sessionid": this.getSessionID(),
"follow": 1
},
"json": true
}, (err, res, body) => {
if (!callback) {
return;
}
if (err) {
callback(err);
return;
}
if (body.success && body.success.success != SteamCommunity.EResult.OK) {
callback(Helpers.eresultError(body.success.success));
return;
}
callback(null);
}, "steamcommunity");
};
/**
* Unfollows a curator page
* @param {string|number} curatorId - ID of the curator (not a SteamID)
* @param {function} callback - Takes only an Error object/null as the first argument
*/
SteamCommunity.prototype.unfollowCurator = function(curatorId, callback) {
this.httpRequestPost({
"uri": "https://store.steampowered.com/curators/ajaxfollow",
"form": {
"clanid": curatorId,
"sessionid": this.getSessionID(),
"follow": 0
},
"json": true
}, (err, res, body) => {
if (!callback) {
return;
}
if (err) {
callback(err);
return;
}
if (body.success && body.success.success != SteamCommunity.EResult.OK) {
callback(Helpers.eresultError(body.success.success));
return;
}
callback(null);
}, "steamcommunity");
};

64
components/help.js Normal file
View File

@@ -0,0 +1,64 @@
const SteamCommunity = require('../index.js');
const Helpers = require('./helpers.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
*/
SteamCommunity.prototype.restorePackage = function(packageID, callback) {
this.httpRequestPost({
uri: HELP_SITE_DOMAIN + '/wizard/AjaxDoPackageRestore',
form: {
packageid: packageID,
sessionid: this.getSessionID(HELP_SITE_DOMAIN),
wizard_ajax: 1
},
json: true
}, wizardAjaxHandler(callback));
};
/**
* Remove a steam package from your steam account.
* @param {int|string} packageID
* @param {function} callback
*/
SteamCommunity.prototype.removePackage = function(packageID, callback) {
this.httpRequestPost({
uri: HELP_SITE_DOMAIN + '/wizard/AjaxDoPackageRemove',
form: {
packageid: packageID,
sessionid: this.getSessionID(HELP_SITE_DOMAIN),
wizard_ajax: 1
},
json: true
}, wizardAjaxHandler(callback));
};
/**
* Returns a handler for wizard ajax HTTP requests.
* @param {function} callback
* @returns {(function(*=, *, *): void)|*}
*/
function wizardAjaxHandler(callback) {
return (err, res, body) => {
if (!callback) {
return;
}
if (err) {
callback(err);
return;
}
if (!body.success) {
callback(body.errorMsg ? new Error(body.errorMsg) : Helpers.eresultError(body.success));
return;
}
callback(null);
};
}

View File

@@ -1,3 +1,9 @@
const request = require('request');
const SteamID = require('steamid');
const xml2js = require('xml2js');
const EResult = require('../resources/EResult.js');
exports.isSteamID = function(input) {
var keys = Object.keys(input);
if (keys.length != 4) {
@@ -13,11 +19,110 @@ exports.isSteamID = function(input) {
};
exports.decodeSteamTime = function(time) {
var parts = time.split('@');
if (!parts[0].match(/,/)) {
// no year, assume current year
parts[0] += ", " + (new Date()).getFullYear();
var date = new Date();
if (time.includes("@")) {
var 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
var 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
* @returns {null|Error}
*/
exports.eresultError = function(eresult) {
if (eresult == EResult.OK) {
// no error
return null;
}
var err = new Error(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'));
};
/**
* Resolves a Steam profile URL to get steamID64 and vanityURL
* @param {String} url - Full steamcommunity profile URL or only the vanity part.
* @param {Object} callback - First argument is null/Error, second is object containing vanityURL (String) and steamID (String)
*/
exports.resolveVanityURL = function(url, callback) {
// 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
request(url + "/?xml=1", function(err, response, body) {
if (err) {
callback(err);
return;
}
// Parse XML data returned from Steam into an object
new xml2js.Parser().parseString(body, (err, parsed) => {
if (err) {
callback(new Error("Couldn't parse XML response"));
return;
}
if (parsed.response && parsed.response.error) {
callback(new Error("Couldn't find Steam ID"));
return;
}
let steamID64 = parsed.profile.steamID64[0];
let vanityURL = parsed.profile.customURL[0];
callback(null, {"vanityURL": vanityURL, "steamID": steamID64});
});
});
};
/**
* Converts `input` into a SteamID object, if it's a parseable string.
* @param {SteamID|string} input
* @return {SteamID}
*/
exports.steamID = function(input) {
if (exports.isSteamID(input)) {
return input;
}
if (typeof input != 'string') {
throw new Error(`Input SteamID value "${input}" is not a string`);
}
// This will throw if the input is not a well-formed SteamID
return new SteamID(input);
};

View File

@@ -1,3 +1,5 @@
var URL = require('url');
var SteamCommunity = require('../index.js');
SteamCommunity.prototype.httpRequest = function(uri, options, callback, source) {
@@ -19,6 +21,16 @@ SteamCommunity.prototype.httpRequest = function(uri, options, callback, source)
delete this._httpRequestConvenienceMethod;
}
// Add origin header if necessary
// https://github.com/DoctorMcKay/node-steamcommunity/issues/351
if ((options.method || 'GET').toUpperCase() != 'GET') {
options.headers = options.headers || {};
if (!options.headers.origin) {
var parsedUrl = URL.parse(options.url);
options.headers.origin = parsedUrl.protocol + '//' + parsedUrl.host;
}
}
var requestID = ++this._httpRequestID;
source = source || "";
@@ -47,19 +59,25 @@ SteamCommunity.prototype.httpRequest = function(uri, options, callback, source)
self.request(options, function (err, response, body) {
var hasCallback = !!callback;
var httpError = options.checkHttpError !== false && self._checkHttpError(err, response, 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 || null, response, body, {
self.emit('postHttpRequest', requestID, source, options, httpError || communityError || tradeError || jsonError || null, response, body, {
"hasCallback": hasCallback,
"httpError": httpError,
"communityError": communityError,
"tradeError": tradeError
"tradeError": tradeError,
"jsonError": jsonError
});
if(hasCallback && !(httpError || communityError || tradeError)) {
callback.apply(self, arguments);
if (hasCallback && !(httpError || communityError || tradeError)) {
if (jsonError) {
callback.call(self, jsonError, response);
} else {
callback.apply(self, arguments);
}
}
});
}
@@ -79,29 +97,29 @@ SteamCommunity.prototype._notifySessionExpired = function(err) {
this.emit('sessionExpired', err);
};
SteamCommunity.prototype._checkHttpError = function(err, response, callback) {
SteamCommunity.prototype._checkHttpError = function(err, response, callback, body) {
if (err) {
callback(err);
callback(err, response, body);
return err;
}
if (response.statusCode >= 300 && response.statusCode <= 399 && response.headers.location.indexOf('/login') != -1) {
err = new Error("Not Logged In");
callback(err);
callback(err, response, body);
this._notifySessionExpired(err);
return err;
}
if (response.statusCode == 403 && response.body && response.body.match(/<div id="parental_notice_instructions">Enter your PIN below to exit Family View.<\/div>/)) {
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>/)) {
err = new Error("Family View Restricted");
callback(err);
callback(err, response, body);
return err;
}
if (response.statusCode >= 400) {
err = new Error("HTTP error " + response.statusCode);
err.code = response.statusCode;
callback(err);
callback(err, response, body);
return err;
}
@@ -118,7 +136,7 @@ SteamCommunity.prototype._checkCommunityError = function(html, callback) {
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");
callback(err);
this._notifySessionExpired(err);

View File

@@ -1,122 +1,168 @@
var SteamCommunity = require('../index.js');
var CEconItem = require('../classes/CEconItem.js');
var Helpers = require('./helpers.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') {
if (typeof options === 'function') {
callback = options;
options = {};
}
options.page = options.page || 1;
this.httpRequest("https://steamcommunity.com/my/inventoryhistory?l=english&p=" + options.page, function(err, response, body) {
if(err) {
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);
var html = $('.inventory_history_pagingrow').html();
if(!html) {
callback("Malformed page: no paging row found");
if (!$('.inventory_history_pagingrow').html()) {
callback(new Error("Malformed page: no paging row found"));
return;
}
var match = html.match(/(\d+) - (\d+) of (\d+) History Items/);
output.first = parseInt(match[1], 10);
output.last = parseInt(match[2], 10);
output.totalTrades = parseInt(match[3], 10);
// Load the inventory item data
var match2 = body.match(/var g_rgHistoryInventory = (.*);/);
if(!match2) {
if (!match2) {
callback(new Error("Malformed page: no trade found"));
return;
}
var historyInventory = JSON.parse(match2[1]);
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(var i = 0; i < trades.length; i++) {
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') {
if (timeMatch[1] == 12 && timeMatch[3] == 'am') {
timeMatch[1] = 0;
}
if(timeMatch[1] < 12 && timeMatch[3] == 'pm') {
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);
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) {
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) {
if (options.resolveVanityURLs && vanityURLs.indexOf(trade.partnerVanityURL) == -1) {
vanityURLs.push(trade.partnerVanityURL);
}
}
items = item.find('.history_item');
for(j = 0; j < items.length; j++) {
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) {
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) {
if (options.resolveVanityURLs) {
Async.map(vanityURLs, Helpers.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) {
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) {
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 {
@@ -125,19 +171,3 @@ SteamCommunity.prototype.getInventoryHistory = function(options, callback) {
}, "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]});
});
}

110
components/login.js Normal file
View File

@@ -0,0 +1,110 @@
const {chrome} = require('@doctormckay/user-agents');
const SteamCommunity = require('../index.js');
/**
* @typedef LogOnDetails
* @property {string} accountName
* @property {string} password
* @property {string} [steamguard]
* @property {string} [authCode]
* @property {string} [twoFactorCode]
* @property {boolean} disableMobile
*/
/**
* @typedef LogOnResponse
* @property {string} sessionID
* @property {string[]} cookies
* @property {string} steamguard
* @property {string} [mobileAccessToken]
*/
/**
*
* @param {LogOnDetails} logOnDetails
* @returns {Promise<LogOnResponse>}
* @private
*/
SteamCommunity.prototype._modernLogin = function(logOnDetails) {
return new Promise(async (resolve, reject) => {
if (!isNodeVersionNewEnough()) {
return reject(new Error(`Node.js version is too old! Need >=12.22.0 or later, got ${process.versions.node}.`));
}
if (this._options.request) {
return reject(new Error('SteamCommunity.login() is incompatible with node-steamcommunity v3\'s usage of \'request\'. If you need to specify a custom \'request\' instance (e.g. when using a proxy), use https://www.npmjs.com/package/steam-session directly to log onto Steam.'));
}
// Import this here so we don't cause problems on old Node versions if this code path isn't taken.
const {LoginSession, EAuthTokenPlatformType, EAuthSessionGuardType} = require('steam-session');
let session = new LoginSession(
logOnDetails.disableMobile
? EAuthTokenPlatformType.WebBrowser
: EAuthTokenPlatformType.MobileApp,
{
localAddress: this._options.localAddress,
userAgent: this._options.userAgent || chrome()
}
);
session.on('authenticated', async () => {
try {
let webCookies = await session.getWebCookies();
let sessionIdCookie = webCookies.find(c => c.startsWith('sessionid='));
resolve({
sessionID: sessionIdCookie.split('=')[1].split(';')[0].trim(),
cookies: webCookies,
steamguard: session.steamGuardMachineToken,
mobileAccessToken: logOnDetails.disableMobile ? null : session.accessToken
});
} catch (ex) {
reject(ex);
}
});
session.on('error', (err) => {
reject(err);
});
try {
let startResult = await session.startWithCredentials({
accountName: logOnDetails.accountName,
password: logOnDetails.password,
steamGuardMachineToken: logOnDetails.steamguard,
steamGuardCode: logOnDetails.authCode || logOnDetails.twoFactorCode
});
if (startResult.actionRequired) {
// Cannot continue with login, need something from the user
session.cancelLoginAttempt();
let emailMfaAction = startResult.validActions.find(action => action.type == EAuthSessionGuardType.EmailCode);
if (emailMfaAction) {
let err = new Error('SteamGuard');
err.emaildomain = emailMfaAction.detail;
return reject(err);
}
return reject(new Error('SteamGuardMobile'));
}
} catch (ex) {
return reject(ex);
}
});
};
function isNodeVersionNewEnough() {
let [major, minor] = process.versions.node.split('.');
if (major < 12) {
return false;
}
if (major == 12 && minor < 22) {
return false;
}
return true;
}

View File

@@ -1,6 +1,12 @@
var SteamCommunity = require('../index.js');
var Cheerio = require('cheerio');
const SteamCommunity = require('../index.js');
const Cheerio = require('cheerio');
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) {
var self = this;
this.httpRequest('https://steamcommunity.com/market/', function (err, response, body) {
@@ -11,8 +17,8 @@ SteamCommunity.prototype.getMarketApps = function(callback) {
var $ = Cheerio.load(body);
if ($('.market_search_game_button_group')) {
apps = {};
$('.market_search_game_button_group > a').each(function (i, element) {
let apps = {};
$('.market_search_game_button_group a.game_button').each(function (i, element) {
var e = Cheerio.load(element);
var name = e('.game_button_game_name').text().trim();
var url = element.attribs.href;
@@ -25,3 +31,357 @@ SteamCommunity.prototype.getMarketApps = function(callback) {
}
}, "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;
}
if (body.success && body.success != SteamCommunity.EResult.OK) {
let err = new Error(body.message || SteamCommunity.EResult[body.success]);
err.eresult = err.code = body.success;
callback(err);
return;
}
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;
}
if (body.success && body.success != SteamCommunity.EResult.OK) {
let err = new Error(body.message || SteamCommunity.EResult[body.success]);
err.eresult = err.code = body.success;
callback(err);
return;
}
if (!body['goo_value_received '] || !body.goo_value_total) { // lol valve
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;
}
if (body.success && body.success != SteamCommunity.EResult.OK) {
let err = new Error(body.message || SteamCommunity.EResult[body.success]);
err.eresult = err.code = body.success;
callback(err);
return;
}
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({
uri: '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)) {
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({
"uri": "https://steamcommunity.com/gifts/" + giftID + "/validateunpack",
"form": {
"sessionid": this.getSessionID()
},
"json": true
}, (err, res, body) => {
if (err) {
callback(err);
return;
}
if (body.success && body.success != SteamCommunity.EResult.OK) {
let err = new Error(body.message || SteamCommunity.EResult[body.success]);
err.eresult = err.code = body.success;
callback(err);
return;
}
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({
"uri": "https://steamcommunity.com/gifts/" + giftID + "/unpack",
"form": {
"sessionid": this.getSessionID()
},
"json": true
}, (err, res, body) => {
if (err) {
callback(err);
return;
}
if (body.success && body.success != SteamCommunity.EResult.OK) {
let err = new Error(body.message || SteamCommunity.EResult[body.success]);
err.eresult = err.code = body.success;
callback(err);
return;
}
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,7 +1,9 @@
var SteamCommunity = require('../index.js');
var SteamID = require('steamid');
var Cheerio = require('cheerio');
var fs = require('fs');
const Cheerio = require('cheerio');
const FS = require('fs');
const SteamID = require('steamid');
const Helpers = require('./helpers.js');
const SteamCommunity = require('../index.js');
SteamCommunity.PrivacyState = {
"Private": 1,
@@ -10,9 +12,9 @@ SteamCommunity.PrivacyState = {
};
var CommentPrivacyState = {
"1": "commentselfonly",
"2": "commentfriendsonly",
"3": "commentanyone"
"1": 2, // private
"2": 0, // friends only
"3": 1 // anyone
};
SteamCommunity.prototype.setupProfile = function(callback) {
@@ -21,7 +23,7 @@ SteamCommunity.prototype.setupProfile = function(callback) {
if(!callback) {
return;
}
if(err || response.statusCode != 200) {
callback(err || new Error("HTTP error " + response.statusCode));
} else {
@@ -32,182 +34,221 @@ SteamCommunity.prototype.setupProfile = function(callback) {
SteamCommunity.prototype.editProfile = function(settings, callback) {
var self = this;
this._myProfile("edit", null, function(err, response, body) {
if(err || response.statusCode != 200) {
if(callback) {
callback(err || new Error("HTTP error " + response.statusCode));
this._myProfile('edit/info', null, function(err, response, body) {
if (err || response.statusCode != 200) {
if (callback) {
callback(err || new Error('HTTP error ' + response.statusCode));
}
return;
}
var $ = Cheerio.load(body);
var form = $('#editForm');
if(!form) {
if(callback) {
callback(new Error("Malformed response"));
var existingSettings = $('#profile_edit_config').data('profile-edit');
if (!existingSettings || !existingSettings.strPersonaName) {
if (callback) {
callback(new Error('Malformed response'));
}
return;
}
var values = {};
form.serializeArray().forEach(function(item) {
values[item.name] = item.value;
});
for(var i in settings) {
var values = {
sessionID: self.getSessionID(),
type: 'profileSave',
weblink_1_title: '',
weblink_1_url: '',
weblink_2_title: '',
weblink_2_url: '',
weblink_3_title: '',
weblink_3_url: '',
personaName: existingSettings.strPersonaName,
real_name: existingSettings.strRealName,
summary: existingSettings.strSummary,
country: existingSettings.LocationData.locCountryCode,
state: existingSettings.LocationData.locStateCode,
city: existingSettings.LocationData.locCityCode,
customURL: existingSettings.strCustomURL,
json: 1
};
for (var i in settings) {
if(!settings.hasOwnProperty(i)) {
continue;
}
switch(i) {
case 'name':
values.personaName = settings[i];
break;
case 'realName':
values.real_name = settings[i];
break;
case 'summary':
values.summary = settings[i];
break;
case 'country':
values.country = settings[i];
break;
case 'state':
values.state = settings[i];
break;
case 'city':
values.city = settings[i];
break;
case 'customURL':
values.customURL = settings[i];
break;
case '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':
if(typeof settings[i] === 'object' && settings[i].getSteamID64) {
values.primary_group_steamid = settings[i].getSteamID64();
} else {
values.primary_group_steamid = new SteamID(settings[i]).getSteamID64();
}
break;
// These don't work right now
/*
case 'background':
// The assetid of our desired profile background
values.profile_background = settings[i];
break;
case 'featuredBadge':
// Currently, game badges aren't supported
values.favorite_badge_badgeid = settings[i];
break;
*/
// TODO: profile showcases
}
}
self._myProfile("edit", values, function(err, response, body) {
self._myProfile('edit', values, function(err, response, body) {
if (settings.customURL) {
delete self._profileURL;
}
if(err || response.statusCode != 200) {
if(callback) {
callback(err || new Error("HTTP error " + response.statusCode));
}
if (!callback) {
return;
}
// Check for an error
var $ = Cheerio.load(body);
var error = $('#errorText .formRowFields');
if(error) {
error = error.text().trim();
if(error) {
if(callback) {
callback(new Error(error));
}
if (err || response.statusCode != 200) {
callback(err || new Error('HTTP error ' + response.statusCode));
return;
}
try {
var json = JSON.parse(body);
if (!json.success || json.success != 1) {
callback(new Error(json.errmsg || 'Request was not successful'));
return;
}
}
if(callback) {
callback(null);
} catch (ex) {
callback(ex);
}
});
});
};
SteamCommunity.prototype.profileSettings = function(settings, callback) {
var self = this;
this._myProfile("edit/settings", null, function(err, response, body) {
if(err || response.statusCode != 200) {
if(callback) {
callback(err || new Error("HTTP error " + response.statusCode));
this._myProfile('edit/settings', null, (err, response, body) => {
if (err || response.statusCode != 200) {
if (callback) {
callback(err || new Error('HTTP error ' + response.statusCode));
}
return;
}
var $ = Cheerio.load(body);
var form = $('#editForm');
if(!form) {
if(callback) {
callback(new Error("Malformed response"));
var existingSettings = $('#profile_edit_config').data('profile-edit');
if (!existingSettings || !existingSettings.Privacy) {
if (callback) {
callback(new Error('Malformed response'));
}
return;
}
var values = {};
form.serializeArray().forEach(function(item) {
values[item.name] = item.value;
});
for(var i in settings) {
if(!settings.hasOwnProperty(i)) {
// PrivacySettings => {PrivacyProfile, PrivacyInventory, PrivacyInventoryGifts, PrivacyOwnedGames, PrivacyPlaytime}
// eCommentPermission
var privacy = existingSettings.Privacy.PrivacySettings;
var commentPermission = existingSettings.Privacy.eCommentPermission;
for (var i in settings) {
if (!settings.hasOwnProperty(i)) {
continue;
}
switch(i) {
switch (i) {
case 'profile':
values.privacySetting = settings[i];
privacy.PrivacyProfile = settings[i];
break;
case 'comments':
values.commentSetting = CommentPrivacyState[settings[i]];
commentPermission = CommentPrivacyState[settings[i]];
break;
case 'inventory':
values.inventoryPrivacySetting = settings[i];
privacy.PrivacyInventory = settings[i];
break;
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;
}
}
self._myProfile("edit/settings", values, function(err, response, body) {
if(err || response.statusCode != 200) {
if(callback) {
callback(err || new Error("HTTP error " + response.statusCode));
this._myProfile({
method: 'POST',
endpoint: 'ajaxsetprivacy/',
json: true,
formData: { // it's multipart because lolvalve
sessionid: this.getSessionID(),
Privacy: JSON.stringify(privacy),
eCommentPermission: commentPermission
}
}, null, function(err, response, body) {
if (err || response.statusCode != 200) {
if (callback) {
callback(err || new Error('HTTP error ' + response.statusCode));
}
return;
}
if(callback) {
callback(null);
if (body.success != 1) {
if (callback) {
callback(new Error(body.success ? 'Error ' + body.success : 'Request was not successful'));
}
return;
}
if (callback) {
callback(null, body.Privacy);
}
});
});
@@ -219,6 +260,12 @@ SteamCommunity.prototype.uploadAvatar = function(image, format, callback) {
format = null;
}
// are we logged in?
if (!this.steamID) {
callback(new Error("Not Logged In"));
return;
}
var self = this;
if(image instanceof Buffer) {
@@ -250,7 +297,7 @@ SteamCommunity.prototype.uploadAvatar = function(image, format, callback) {
}
}
fs.readFile(image, function(err, file) {
FS.readFile(image, function(err, file) {
if(err) {
if(callback) {
callback(err);
@@ -361,3 +408,68 @@ SteamCommunity.prototype.uploadAvatar = function(image, format, callback) {
}, "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;
}
var 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);
}
});
};

158
components/sharedfiles.js Normal file
View File

@@ -0,0 +1,158 @@
var SteamID = require('steamid');
var 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
*/
SteamCommunity.prototype.deleteSharedFileComment = function(userID, sharedFileId, cid, callback) {
if (typeof userID === "string") {
userID = new SteamID(userID);
}
this.httpRequestPost({
"uri": `https://steamcommunity.com/comment/PublishedFile_Public/delete/${userID.toString()}/${sharedFileId}/`,
"form": {
"gidcomment": cid,
"count": 10,
"sessionid": this.getSessionID()
}
}, function(err, response, body) {
if (!callback) {
return;
}
callback(err);
}, "steamcommunity");
};
/**
* 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
*/
SteamCommunity.prototype.favoriteSharedFile = function(sharedFileId, appid, callback) {
this.httpRequestPost({
"uri": "https://steamcommunity.com/sharedfiles/favorite",
"form": {
"id": sharedFileId,
"appid": appid,
"sessionid": this.getSessionID()
}
}, function(err, response, body) {
if (!callback) {
return;
}
callback(err);
}, "steamcommunity");
};
/**
* 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
*/
SteamCommunity.prototype.postSharedFileComment = function(userID, sharedFileId, message, callback) {
if (typeof userID === "string") {
userID = new SteamID(userID);
}
this.httpRequestPost({
"uri": `https://steamcommunity.com/comment/PublishedFile_Public/post/${userID.toString()}/${sharedFileId}/`,
"form": {
"comment": message,
"count": 10,
"sessionid": this.getSessionID()
}
}, function(err, response, body) {
if (!callback) {
return;
}
callback(err);
}, "steamcommunity");
};
/**
* 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
*/
SteamCommunity.prototype.subscribeSharedFileComments = function(userID, sharedFileId, callback) {
if (typeof userID === "string") {
userID = new SteamID(userID);
}
this.httpRequestPost({
"uri": `https://steamcommunity.com/comment/PublishedFile_Public/subscribe/${userID.toString()}/${sharedFileId}/`,
"form": {
"count": 10,
"sessionid": this.getSessionID()
}
}, function(err, response, body) { // eslint-disable-line
if (!callback) {
return;
}
callback(err);
}, "steamcommunity");
};
/**
* 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
*/
SteamCommunity.prototype.unfavoriteSharedFile = function(sharedFileId, appid, callback) {
this.httpRequestPost({
"uri": "https://steamcommunity.com/sharedfiles/unfavorite",
"form": {
"id": sharedFileId,
"appid": appid,
"sessionid": this.getSessionID()
}
}, function(err, response, body) {
if (!callback) {
return;
}
callback(err);
}, "steamcommunity");
};
/**
* 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);
}
this.httpRequestPost({
"uri": `https://steamcommunity.com/comment/PublishedFile_Public/unsubscribe/${userID.toString()}/${sharedFileId}/`,
"form": {
"count": 10,
"sessionid": this.getSessionID()
}
}, function(err, response, body) { // eslint-disable-line
if (!callback) {
return;
}
callback(err);
}, "steamcommunity");
};

View File

@@ -2,158 +2,151 @@ var SteamTotp = require('steam-totp');
var SteamCommunity = require('../index.js');
var 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.
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.
};
SteamCommunity.prototype.enableTwoFactor = function(callback) {
var self = this;
this._verifyMobileAccessToken();
this.getWebApiOauthToken(function(err, token) {
if(err) {
if (!this.mobileAccessToken) {
callback(new Error('No mobile access token available. Provide one by calling setMobileAppAccessToken()'));
return;
}
this.httpRequestPost({
uri: "https://api.steampowered.com/ITwoFactorService/AddAuthenticator/v1/?access_token=" + this.mobileAccessToken,
// TODO: Send this as protobuf to more closely mimic official app behavior
form: {
steamid: this.steamID.getSteamID64(),
authenticator_type: ETwoFactorTokenType.ValveMobileApp,
device_identifier: SteamTotp.getDeviceID(this.steamID),
sms_phone_id: '1',
version: 2
},
json: true
}, (err, response, body) => {
if (err) {
callback(err);
return;
}
self.httpRequestPost({
"uri": "https://api.steampowered.com/ITwoFactorService/AddAuthenticator/v1/",
"form": {
"steamid": self.steamID.getSteamID64(),
"access_token": token,
"authenticator_time": Math.floor(Date.now() / 1000),
"authenticator_type": ETwoFactorTokenType.ValveMobileApp,
"device_identifier": SteamTotp.getDeviceID(self.steamID),
"sms_phone_id": "1"
},
"json": true
}, function(err, response, body) {
if (err) {
callback(err);
return;
}
if (!body.response) {
callback(new Error('Malformed response'));
return;
}
if(!body.response) {
callback(new Error("Malformed response"));
return;
}
if (body.response.status != 1) {
var error = new Error('Error ' + body.response.status);
error.eresult = body.response.status;
callback(error);
return;
}
if(body.response.status != 1) {
var error = new Error("Error " + body.response.status);
error.eresult = body.response.status;
callback(error);
return;
}
callback(null, body.response);
}, "steamcommunity");
});
callback(null, body.response);
}, 'steamcommunity');
};
SteamCommunity.prototype.finalizeTwoFactor = function(secret, activationCode, callback) {
var attemptsLeft = 30;
var diff = 0;
this._verifyMobileAccessToken();
var self = this;
this.getWebApiOauthToken(function(err, token) {
if(err) {
callback(err);
return;
}
if (!this.mobileAccessToken) {
callback(new Error('No mobile access token available. Provide one by calling setMobileAppAccessToken()'));
return;
}
SteamTotp.getTimeOffset(function(err, offset, latency) {
if (err) {
callback(err);
return;
}
let attemptsLeft = 30;
let diff = 0;
diff = offset;
finalize(token);
});
});
let finalize = () => {
let code = SteamTotp.generateAuthCode(secret, diff);
function finalize(token) {
var code = SteamTotp.generateAuthCode(secret, diff);
self.httpRequestPost({
"uri": "https://api.steampowered.com/ITwoFactorService/FinalizeAddAuthenticator/v1/",
"form": {
"steamid": self.steamID.getSteamID64(),
"access_token": token,
"authenticator_code": code,
"authenticator_time": Math.floor(Date.now() / 1000),
"activation_code": activationCode
this.httpRequestPost({
uri: 'https://api.steampowered.com/ITwoFactorService/FinalizeAddAuthenticator/v1/?access_token=' + this.mobileAccessToken,
form: {
steamid: this.steamID.getSteamID64(),
authenticator_code: code,
authenticator_time: Math.floor(Date.now() / 1000),
activation_code: activationCode
},
"json": true
json: true
}, function(err, response, body) {
if (err) {
callback(err);
return;
}
if(!body.response) {
callback(new Error("Malformed response"));
if (!body.response) {
callback(new Error('Malformed response'));
return;
}
body = body.response;
if(body.server_time) {
if (body.server_time) {
diff = body.server_time - Math.floor(Date.now() / 1000);
}
if(body.status == 89) {
callback(new Error("Invalid activation code"));
if (body.status == 89) {
callback(new Error('Invalid activation code'));
} else if(body.want_more) {
attemptsLeft--;
diff += 30;
finalize(token);
finalize();
} else if(!body.success) {
callback(new Error("Error " + body.status));
callback(new Error('Error ' + body.status));
} else {
callback(null);
}
}, "steamcommunity");
}, 'steamcommunity');
}
};
SteamCommunity.prototype.disableTwoFactor = function(revocationCode, callback) {
var self = this;
this.getWebApiOauthToken(function(err, token) {
if(err) {
SteamTotp.getTimeOffset(function(err, offset, latency) {
if (err) {
callback(err);
return;
}
self.httpRequestPost({
"uri": "https://api.steampowered.com/ITwoFactorService/RemoveAuthenticator/v1/",
"form": {
"steamid": self.steamID.getSteamID64(),
"access_token": token,
"revocation_code": revocationCode,
"steamguard_scheme": 1
},
"json": true
}, function(err, response, body) {
if (err) {
callback(err);
return;
}
if(!body.response) {
callback(new Error("Malformed response"));
return;
}
if(!body.response.success) {
callback(new Error("Request failed"));
return;
}
// success = true means it worked
callback(null);
}, "steamcommunity");
diff = offset;
finalize();
});
};
SteamCommunity.prototype.disableTwoFactor = function(revocationCode, callback) {
this._verifyMobileAccessToken();
if (!this.mobileAccessToken) {
callback(new Error('No mobile access token available. Provide one by calling setMobileAppAccessToken()'));
return;
}
this.httpRequestPost({
uri: 'https://api.steampowered.com/ITwoFactorService/RemoveAuthenticator/v1/?access_token=' + this.mobileAccessToken,
form: {
steamid: this.steamID.getSteamID64(),
revocation_code: revocationCode,
steamguard_scheme: 1
},
json: true
}, function(err, response, body) {
if (err) {
callback(err);
return;
}
if (!body.response) {
callback(new Error('Malformed response'));
return;
}
if (!body.response.success) {
callback(new Error('Request failed'));
return;
}
// success = true means it worked
callback(null);
}, 'steamcommunity');
};

View File

@@ -1,7 +1,12 @@
var SteamCommunity = require('../index.js');
var SteamID = require('steamid');
var CEconItem = require('../classes/CEconItem.js');
var Helpers = require('./helpers.js');
const Cheerio = require('cheerio');
const Crypto = require('crypto');
const imageSize = require('image-size');
const SteamID = require('steamid');
const SteamCommunity = require('../index.js');
const CEconItem = require('../classes/CEconItem.js');
const Helpers = require('./helpers.js');
SteamCommunity.prototype.addFriend = function(userID, callback) {
if(typeof userID === 'string') {
@@ -131,7 +136,7 @@ SteamCommunity.prototype.postUserComment = function(userID, message, callback) {
"uri": "https://steamcommunity.com/comment/Profile/post/" + userID.toString() + "/-1",
"form": {
"comment": message,
"count": 6,
"count": 1,
"sessionid": this.getSessionID()
},
"json": true
@@ -146,9 +151,109 @@ SteamCommunity.prototype.postUserComment = function(userID, message, callback) {
}
if(body.success) {
const $ = Cheerio.load(body.comments_html);
const commentID = $('.commentthread_comment').attr('id').split('_')[1];
callback(null, commentID);
} else if(body.error) {
callback(new Error(body.error));
} else {
callback(new Error("Unknown error"));
}
}, "steamcommunity");
};
SteamCommunity.prototype.deleteUserComment = function(userID, commentID, callback) {
if(typeof userID === 'string') {
userID = new SteamID(userID);
}
var self = this;
this.httpRequestPost({
"uri": "https://steamcommunity.com/comment/Profile/delete/" + userID.toString() + "/-1",
"form": {
"gidcomment": commentID,
"start": 0,
"count": 1,
"sessionid": this.getSessionID(),
"feature2": -1
},
"json": true
}, function(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 = {};
}
var form = Object.assign({
"start": 0,
"count": 0,
"feature2": -1,
"sessionid": this.getSessionID()
}, options);
this.httpRequestPost({
"uri": "https://steamcommunity.com/comment/Profile/render/" + userID.toString() + "/-1",
"form": form,
"json": true
}, function(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) => {
var $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"));
}
@@ -191,6 +296,66 @@ SteamCommunity.prototype.inviteUserToGroup = function(userID, groupID, callback)
}, "steamcommunity");
};
SteamCommunity.prototype.followUser = function(userID, callback) {
if(typeof userID === 'string') {
userID = new SteamID(userID);
}
this.httpRequestPost({
"uri": `https://steamcommunity.com/profiles/${userID.toString()}/followuser/`,
"form": {
"sessionid": this.getSessionID(),
},
"json": true
}, function(err, response, body) {
if (!callback) {
return;
}
if (err) {
callback(err);
return;
}
if (body.success && body.success != SteamCommunity.EResult.OK) {
callback(Helpers.eresultError(body.success));
return;
}
callback(null);
}, "steamcommunity");
};
SteamCommunity.prototype.unfollowUser = function(userID, callback) {
if(typeof userID === 'string') {
userID = new SteamID(userID);
}
this.httpRequestPost({
"uri": `https://steamcommunity.com/profiles/${userID.toString()}/unfollowuser/`,
"form": {
"sessionid": this.getSessionID(),
},
"json": true
}, function(err, response, body) {
if (!callback) {
return;
}
if (err) {
callback(err);
return;
}
if (body.success && body.success != SteamCommunity.EResult.OK) {
callback(Helpers.eresultError(body.success));
return;
}
callback(null);
}, "steamcommunity");
};
SteamCommunity.prototype.getUserAliases = function(userID, callback) {
if (typeof userID === 'string') {
userID = new SteamID(userID);
@@ -217,17 +382,56 @@ SteamCommunity.prototype.getUserAliases = function(userID, callback) {
}, "steamcommunity");
};
SteamCommunity.prototype.getUserInventoryContexts = function(userID, callback) {
if(typeof userID === 'string') {
/**
* 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);
}
if(typeof userID === 'function') {
this.httpRequest("https://steamcommunity.com/profiles/" + userID.getSteamID64(), (err, response, body) => {
if (err) {
callback(err);
return;
}
var $ = Cheerio.load(body);
var $privateProfileInfo = $('.profile_private_info');
if ($privateProfileInfo.length > 0) {
callback(new Error($privateProfileInfo.text().trim()));
return;
}
if ($('body').hasClass('has_profile_background')) {
var backgroundUrl = $('div.profile_background_image_content').css('background-image');
var matcher = backgroundUrl.match(/\(([^)]+)\)/);
if (matcher.length != 2 || !matcher[1].length) {
callback(new Error("Malformed response"));
} else {
callback(null, matcher[1]);
}
} else {
callback(null, null);
}
}, "steamcommunity");
};
SteamCommunity.prototype.getUserInventoryContexts = function(userID, callback) {
if (typeof userID === 'string') {
userID = new SteamID(userID);
}
if (typeof userID === 'function') {
callback = userID;
userID = this.steamID;
}
if(!userID) {
if (!userID) {
callback(new Error("No SteamID specified and not logged in"));
return;
}
@@ -240,8 +444,8 @@ SteamCommunity.prototype.getUserInventoryContexts = function(userID, callback) {
}
var match = body.match(/var g_rgAppContextData = ([^\n]+);\r?\n/);
if(!match) {
callback(new Error("Malformed response"));
if (!match) {
callback(new Error('Malformed response'));
return;
}
@@ -253,14 +457,38 @@ SteamCommunity.prototype.getUserInventoryContexts = function(userID, callback) {
return;
}
if (Object.keys(data).length == 0) {
if (body.match(/inventory is currently private\./)) {
callback(new Error('Private inventory'));
return;
}
if (body.match(/profile_private_info/)) {
callback(new Error('Private profile'));
return;
}
// If they truly have no items in their inventory, Steam will send g_rgAppContextData as [] instead of {}.
data = {};
}
callback(null, data);
}, "steamcommunity");
};
/**
* Get the contents of a user's inventory context.
* @deprecated Use getUserInventoryContents instead
* @param {SteamID|string} userID - The user's SteamID as a SteamID object or a string which can parse into one
* @param {int} appID - The Steam application ID of the game for which you want an inventory
* @param {int} contextID - The ID of the "context" within the game you want to retrieve
* @param {boolean} tradableOnly - true to get only tradable items and currencies
* @param {function} callback
*/
SteamCommunity.prototype.getUserInventory = function(userID, appID, contextID, tradableOnly, callback) {
var self = this;
if(typeof userID === 'string') {
if (typeof userID === 'string') {
userID = new SteamID(userID);
}
@@ -270,6 +498,9 @@ SteamCommunity.prototype.getUserInventory = function(userID, appID, contextID, t
function get(inventory, currency, start) {
self.httpRequest({
"uri": "https://steamcommunity.com" + endpoint + "/inventory/json/" + appID + "/" + contextID,
"headers": {
"Referer": "https://steamcommunity.com" + endpoint + "/inventory"
},
"qs": {
"start": start,
"trading": tradableOnly ? 1 : undefined
@@ -281,34 +512,34 @@ SteamCommunity.prototype.getUserInventory = function(userID, appID, contextID, t
return;
}
if(!body || !body.success || !body.rgInventory || !body.rgDescriptions || !body.rgCurrency) {
if(body) {
if (!body || !body.success || !body.rgInventory || !body.rgDescriptions || !body.rgCurrency) {
if (body) {
callback(new Error(body.Error || "Malformed response"));
} else {
callback(new Error("Malformed response"));
}
return;
}
var i;
for(i in body.rgInventory) {
if(!body.rgInventory.hasOwnProperty(i)) {
for (i in body.rgInventory) {
if (!body.rgInventory.hasOwnProperty(i)) {
continue;
}
inventory.push(new CEconItem(body.rgInventory[i], body.rgDescriptions, contextID));
}
for(i in body.rgCurrency) {
if(!body.rgCurrency.hasOwnProperty(i)) {
for (i in body.rgCurrency) {
if (!body.rgCurrency.hasOwnProperty(i)) {
continue;
}
currency.push(new CEconItem(body.rgInventory[i], body.rgDescriptions, contextID));
currency.push(new CEconItem(body.rgCurrency[i], body.rgDescriptions, contextID));
}
if(body.more) {
if (body.more) {
var match = response.request.uri.href.match(/\/(profiles|id)\/([^\/]+)\//);
if(match) {
endpoint = "/" + match[1] + "/" + match[2];
@@ -321,3 +552,296 @@ SteamCommunity.prototype.getUserInventory = function(userID, appID, contextID, t
}, "steamcommunity");
}
};
/**
* Get the contents of a user's inventory context.
* @param {SteamID|string} userID - The user's SteamID as a SteamID object or a string which can parse into one
* @param {int} appID - The Steam application ID of the game for which you want an inventory
* @param {int} contextID - The ID of the "context" within the game you want to retrieve
* @param {boolean} tradableOnly - true to get only tradable items and currencies
* @param {string} [language] - The language of item descriptions to return. Omit for default (which may either be English or your account's chosen language)
* @param {function} callback
*/
SteamCommunity.prototype.getUserInventoryContents = function(userID, appID, contextID, tradableOnly, language, callback) {
if (typeof language === 'function') {
callback = language;
language = "english";
}
if (!userID) {
callback(new Error("The user's SteamID is invalid or missing."));
return;
}
var self = this;
if (typeof userID === 'string') {
userID = new SteamID(userID);
}
var pos = 1;
get([], []);
function get(inventory, currency, start) {
self.httpRequest({
"uri": "https://steamcommunity.com/inventory/" + userID.getSteamID64() + "/" + appID + "/" + contextID,
"headers": {
"Referer": "https://steamcommunity.com/profiles/" + userID.getSteamID64() + "/inventory"
},
"qs": {
"l": language, // Default language
"count": 1000, // Max items per 'page'
"start_assetid": start
},
"json": true
}, function(err, response, body) {
if (err) {
if (err.message == "HTTP error 403" && body === null) {
// 403 with a body of "null" means the inventory/profile is private.
if (self.steamID && userID.getSteamID64() == self.steamID.getSteamID64()) {
// We can never get private profile error for our own inventory!
self._notifySessionExpired(err);
}
callback(new Error("This profile is private."));
return;
}
if (err.message == "HTTP error 500" && body && body.error) {
err = new Error(body.error);
var match = body.error.match(/^(.+) \((\d+)\)$/);
if (match) {
err.message = match[1];
err.eresult = match[2];
callback(err);
return;
}
}
callback(err);
return;
}
if (body && body.success && body.total_inventory_count === 0) {
// Empty inventory
callback(null, [], [], 0);
return;
}
if (appID == 730 && body && body.success && !body.assets) {
// CS inventory has no visible items. We need a special case for this because Valve is incapable of
// doing anything not dumb.
callback(null, [], [], body.total_inventory_count);
return;
}
if (!body || !body.success || !body.assets || !body.descriptions) {
if (body) {
// Dunno if the error/Error property even exists on this new endpoint
callback(new Error(body.error || body.Error || "Malformed response"));
} else {
callback(new Error("Malformed response"));
}
return;
}
// Build asset_properties lookup by assetid
var assetPropertiesLookup = {};
if (body.asset_properties) {
for (var j = 0; j < body.asset_properties.length; j++) {
assetPropertiesLookup[body.asset_properties[j].assetid] = body.asset_properties[j].asset_properties;
}
}
for (var i = 0; i < body.assets.length; i++) {
var description = getDescription(body.descriptions, body.assets[i].classid, body.assets[i].instanceid);
if (!tradableOnly || (description && description.tradable)) {
body.assets[i].pos = pos++;
var assetProperties = assetPropertiesLookup[body.assets[i].assetid] || null;
(body.assets[i].currencyid ? currency : inventory).push(new CEconItem(body.assets[i], description, contextID, assetProperties));
}
}
if (body.more_items) {
get(inventory, currency, body.last_assetid);
} else {
callback(null, inventory, currency, body.total_inventory_count);
}
}, "steamcommunity");
}
// A bit of optimization; objects are hash tables so it's more efficient to look up by key than to iterate an array
var quickDescriptionLookup = {};
function getDescription(descriptions, classID, instanceID) {
var key = classID + '_' + (instanceID || '0'); // instanceID can be undefined, in which case it's 0.
if (quickDescriptionLookup[key]) {
return quickDescriptionLookup[key];
}
for (var i = 0; i < descriptions.length; i++) {
quickDescriptionLookup[descriptions[i].classid + '_' + (descriptions[i].instanceid || '0')] = descriptions[i];
}
return quickDescriptionLookup[key];
}
};
/**
* Upload an image to Steam and send it to another user over Steam chat.
* @param {SteamID|string} userID - Either a SteamID object or a string that can parse into one
* @param {Buffer} imageContentsBuffer - The image contents, as a Buffer
* @param {{spoiler?: boolean}} [options]
* @param {function} callback
*/
SteamCommunity.prototype.sendImageToUser = function(userID, imageContentsBuffer, options, callback) {
if (typeof options == 'function') {
callback = options;
options = {};
}
options = options || {};
if (!userID) {
callback(new Error('The user\'s SteamID is invalid or missing'));
return;
}
if (typeof userID == 'string') {
userID = new SteamID(userID);
}
if (!Buffer.isBuffer(imageContentsBuffer)) {
callback(new Error('The image contents must be a Buffer containing an image'));
return;
}
var imageDetails = null;
try {
imageDetails = imageSize(imageContentsBuffer);
} catch (ex) {
callback(ex);
return;
}
var imageHash = Crypto.createHash('sha1');
imageHash.update(imageContentsBuffer);
imageHash = imageHash.digest('hex');
var filename = Date.now() + '_image.' + imageDetails.type;
this.httpRequestPost({
uri: '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) {
var 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;
}
var hmac = body.hmac;
var timestamp = body.timestamp;
var 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
var uploadUrl = (startResult.use_https ? 'https' : 'http') + '://' + startResult.url_host + startResult.url_path;
var headers = {};
startResult.request_headers.forEach((header) => {
headers[header.name.toLowerCase()] = header.value;
});
this.httpRequest({
uri: uploadUrl,
method: 'PUT',
headers,
body: imageContentsBuffer
}, (err, res, body) => {
if (err) {
callback(err);
return;
}
// Now we need to commit the upload
this.httpRequestPost({
uri: '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,221 @@
var SteamCommunity = require('../index.js');
const SteamCommunity = require('../index.js');
const Helpers = require('./helpers.js');
/**
* Retrieves your account's Steam Web API key, if you already have one. If you don't yet have one, this will fail.
* To create a Web API key, use `createWebApiKey()`.
*
* @param {null|function} unused - No longer used, kept for backward compatibility. You can omit this parameter and pass
* your callback directly as the first parameter if you want.
* @param {function} callback
*/
SteamCommunity.prototype.getWebApiKey = function(unused, callback) {
if (typeof unused == 'function') {
callback = unused;
}
SteamCommunity.prototype.getWebApiKey = function(domain, callback) {
var self = this;
this.httpRequest({
"uri": "https://steamcommunity.com/dev/apikey",
"followRedirect": false
}, function(err, response, body) {
uri: 'https://steamcommunity.com/dev/apikey?l=english',
followRedirect: false
}, (err, response, body) => {
if (err) {
callback(err);
return;
}
if(body.match(/<h2>Access Denied<\/h2>/)) {
return callback(new Error("Access Denied"));
if (body.match(/You must have a validated email address to create a Steam Web API key./)) {
return callback(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>/);
if(match) {
if (body.match(/Your account requires (<a [^>]+>)?Steam Guard Mobile Authenticator/)) {
return callback(new Error('Steam Guard Mobile Authenticator required to create a Steam Web API key'));
}
if (body.match(/<h2>Access Denied<\/h2>/)) {
return callback(new Error('Access Denied'));
}
let match = body.match(/<p>Key: ([0-9A-F]+)<\/p>/);
if (match) {
// We already have an API key registered
callback(null, match[1]);
} else {
// We need to register a new API key
self.httpRequestPost('https://steamcommunity.com/dev/registerkey', {
"form": {
"domain": domain,
"agreeToTerms": "agreed",
"sessionid": self.getSessionID(),
"Submit": "Register"
}
}, function(err, response, body) {
if (err) {
callback(err);
return;
}
self.getWebApiKey(domain, callback);
}, "steamcommunity");
callback(new Error('No API key created for this account'));
}
}, "steamcommunity");
};
SteamCommunity.prototype.getWebApiOauthToken = function(callback) {
var self = this;
/**
* @typedef CreateApiKeyOptions
* @property {string} domain - The domain to associate with your API key
* @property {string} [requestID] - If finalizing an existing create request, include the request ID
* @property {string|Buffer} [identitySecret] - If you pass your identity_secret here, then steamcommunity will
* internally handle accepting any confirmations.
*/
if( this.oAuthToken ) {
return callback( null, this.oAuthToken );
/**
* @typedef CreateApiKeyResponse
* @property {boolean} confirmationRequired
* @property {string} [apiKey] - If creating your API key succeeded, this is the new key
* @property {CreateApiKeyOptions} [finalizeOptions] - If confirmation is required to create a key, then accept the
* confirmation, then call createWebApiKey again and pass this whole object for the `options` parameter.
*/
/**
* @callback createWebApiKeyCallback
* @param {Error|null} err
* @param {CreateApiKeyResponse} [result]
*/
/**
* Starts the process to create a Steam Web API key. When the callback is fired, you will need to approve a mobile
* confirmation in your app or using getConfirmations().
*
* @param {CreateApiKeyOptions} options
* @param {createWebApiKeyCallback} callback
*/
SteamCommunity.prototype.createWebApiKey = function(options, callback) {
if (!options.domain) {
callback(new Error('Passing a domain is required to register an API key'));
return;
}
// Pull an oauth token from the webchat UI
this.httpRequest("https://steamcommunity.com/chat", function(err, response, body) {
this.httpRequestPost({
uri: 'https://steamcommunity.com/dev/requestkey',
form: {
domain: options.domain,
request_id: options.requestID || '0',
sessionid: this.getSessionID(),
agreeToTerms: 'true'
},
json: true
}, (err, res, body) => {
if (err) {
callback(err);
return;
}
var match = body.match(/"([0-9a-f]{32})"/);
if (!match) {
callback(new Error("Malformed response"));
return;
}
// body.requires_confirmation is 1/0, but the Steam website doesn't check this value and instead only checks the
// value of `success`. So let's just do that.
callback(null, match[1]);
}, "steamcommunity");
// This is a mess. I'm glad we have promises and await now.
switch (body.success) {
case SteamCommunity.EResult.OK:
if (body.api_key) {
callback(null, {confirmationRequired: false, apiKey: body.api_key});
return;
}
// It's not been observed that we get result OK without api_key included, but the Steam website doesn't
// use this value so let's be safe just in case it disappears in the future.
this.getWebApiKey((err, key) => {
if (err) {
callback(err);
return;
}
callback(null, {confirmationRequired: false, apiKey: key});
});
return;
case SteamCommunity.EResult.Pending:
let finalizeOptions = {
domain: options.domain,
requestID: body.request_id || options.requestID
}
if (options.identitySecret) {
this.acceptConfirmationForObject(options.identitySecret, finalizeOptions.requestID, (err) => {
if (err) {
callback(err);
} else {
this.createWebApiKey(finalizeOptions, callback);
}
});
return;
}
callback(null, {
confirmationRequired: true,
finalizeOptions: finalizeOptions
});
return;
default:
callback(Helpers.eresultError(body.success));
}
});
};
/**
* @deprecated No longer works. Will be removed in a future release.
* @param {function} callback
*/
SteamCommunity.prototype.getWebApiOauthToken = function(callback) {
if (this.oAuthToken) {
return callback(null, this.oAuthToken);
}
callback(new Error('This operation requires an OAuth token, which is no longer issued by Steam.'));
};
/**
* Sets an access_token generated by steam-session using EAuthTokenPlatformType.MobileApp.
* Required for some operations such as 2FA enabling and disabling.
* This will throw an Error if the provided token is not valid, was not generated for the MobileApp platform, is expired,
* or does not belong to the logged-in user account.
*
* @param {string} token
*/
SteamCommunity.prototype.setMobileAppAccessToken = function(token) {
if (!this.steamID) {
throw new Error('Log on to steamcommunity before setting a mobile app access token');
}
let decodedToken = Helpers.decodeJwt(token);
if (!decodedToken.iss || !decodedToken.sub || !decodedToken.aud || !decodedToken.exp) {
throw new Error('Provided value is not a valid Steam access token');
}
if (decodedToken.iss == 'steam') {
throw new Error('Provided token is a refresh token, not an access token');
}
if (decodedToken.sub != this.steamID.getSteamID64()) {
throw new Error(`Provided token belongs to account ${decodedToken.sub}, but we are logged into ${this.steamID.getSteamID64()}`);
}
if (decodedToken.exp < Math.floor(Date.now() / 1000)) {
throw new Error('Provided token is expired');
}
if ((decodedToken.aud || []).indexOf('mobile') == -1) {
throw new Error('Provided token is not valid for MobileApp platform type');
}
this.mobileAccessToken = token;
};
/**
* Verifies that the mobile access token we already have set is still valid for current login.
*
* @private
*/
SteamCommunity.prototype._verifyMobileAccessToken = function() {
if (!this.mobileAccessToken) {
// No access token, so nothing to do here.
return;
}
let decodedToken = Helpers.decodeJwt(this.mobileAccessToken);
let isTokenInvalid = decodedToken.sub != this.steamID.getSteamID64() // SteamID doesn't match
|| decodedToken.exp < Math.floor(Date.now() / 1000); // Token is expired
if (isTokenInvalid) {
delete this.mobileAccessToken;
}
};

1
examples/.gitignore vendored Normal file
View File

@@ -0,0 +1 @@
twofactor_*.json

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,98 @@
var SteamCommunity = require('../index.js');
var ReadLine = require('readline');
var fs = require('fs');
// 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 SteamTotp = require('steam-totp');
const ReadLine = require('readline');
var community = new SteamCommunity();
var rl = ReadLine.createInterface({
"input": process.stdin,
"output": process.stdout
});
let g_AbortPromptFunc = null;
rl.question("Username: ", function(accountName) {
rl.question("Password: ", function(password) {
rl.question("Two-Factor Auth Code: ", function(authCode) {
rl.question("Revocation Code: R", function(rCode) {
doLogin(accountName, password, authCode, "", rCode);
});
});
});
});
let community = new SteamCommunity();
function doLogin(accountName, password, authCode, captcha, rCode) {
main();
async function main() {
let accountName = await promptAsync('Username: ');
let password = await promptAsync('Password (hidden): ', true);
attemptLogin(accountName, password);
}
function attemptLogin(accountName, password, twoFactorCode) {
community.login({
"accountName": accountName,
"password": password,
"twoFactorCode": authCode,
"captcha": captcha
}, function(err, sessionID, cookies, steamguard) {
if(err) {
if(err.message == 'SteamGuard') {
console.log("This account does not have two-factor authentication enabled.");
process.exit();
return;
accountName,
password,
twoFactorCode,
disableMobile: false
}, async (err) => {
if (err && err.message == 'SteamGuardMobile') {
let code = await promptAsync('Steam Guard App Code OR Shared Secret: ');
if (code.length > 5) {
// If we were provided a shared secret, turn it into a code.
code = SteamTotp.getAuthCode(code);
}
attemptLogin(accountName, password, code);
return;
}
if(err.message == 'CAPTCHA') {
console.log(err.captchaurl);
rl.question("CAPTCHA: ", function(captchaInput) {
doLogin(accountName, password, authCode, captchaInput);
});
if (err) {
throw err;
}
return;
}
doRevoke();
});
}
async function doRevoke() {
let rCode = await promptAsync('Revocation Code: R');
community.disableTwoFactor('R' + rCode, (err) => {
if (err) {
console.log(err);
process.exit();
return;
}
console.log("Logged on!");
community.disableTwoFactor("R" + rCode, function(err) {
if(err) {
console.log(err);
process.exit();
return;
console.log('Two-factor authentication disabled!');
process.exit();
});
}
// Nothing interesting below here, just code for prompting for input from the console.
function promptAsync(question, sensitiveInput = false) {
return new Promise((resolve) => {
let rl = ReadLine.createInterface({
input: process.stdin,
output: sensitiveInput ? null : process.stdout,
terminal: true
});
g_AbortPromptFunc = () => {
rl.close();
resolve('');
};
if (sensitiveInput) {
// We have to write the question manually if we didn't give readline an output stream
process.stdout.write(question);
}
rl.question(question, (result) => {
if (sensitiveInput) {
// We have to manually print a newline
process.stdout.write('\n');
}
console.log("Two-factor authentication disabled!");
process.exit();
g_AbortPromptFunc = null;
rl.close();
resolve(result);
});
});
}
function abortPrompt() {
if (!g_AbortPromptFunc) {
return;
}
g_AbortPromptFunc();
process.stdout.write('\n');
}

View File

@@ -1,6 +1,5 @@
var SteamCommunity = require('../index.js');
var ReadLine = require('readline');
var fs = require('fs');
var community = new SteamCommunity();
var rl = ReadLine.createInterface({

View File

@@ -1,48 +1,56 @@
var SteamCommunity = require('../index.js');
var ReadLine = require('readline');
var fs = require('fs');
// 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 ReadLine = require('readline');
const FS = require('fs');
var community = new SteamCommunity();
var rl = ReadLine.createInterface({
"input": process.stdin,
"output": process.stdout
});
const EResult = SteamCommunity.EResult;
rl.question("Username: ", function(accountName) {
rl.question("Password: ", function(password) {
doLogin(accountName, password);
});
});
let g_AbortPromptFunc = null;
function doLogin(accountName, password, authCode, captcha) {
let community = new SteamCommunity();
main();
async function main() {
let accountName = await promptAsync('Username: ');
let password = await promptAsync('Password (hidden): ', true);
attemptLogin(accountName, password);
}
function attemptLogin(accountName, password, authCode) {
community.login({
"accountName": accountName,
"password": password,
"authCode": authCode,
"captcha": captcha
}, function(err, sessionID, cookies, steamguard) {
if(err) {
if(err.message == 'SteamGuardMobile') {
console.log("This account already has two-factor authentication enabled.");
accountName,
password,
authCode,
disableMobile: false
}, async (err) => {
if (err && err.message == 'SteamGuard') {
let code = await promptAsync('Steam Guard Email Code: ');
attemptLogin(accountName, password, code);
return;
}
if (err) {
throw err;
}
doSetup();
});
}
function doSetup() {
community.enableTwoFactor((err, response) => {
if (err) {
if (err.eresult == EResult.Fail) {
console.log('Error: Failed to enable two-factor authentication. Do you have a phone number attached to your account?');
process.exit();
return;
}
if(err.message == 'SteamGuard') {
console.log("An email has been sent to your address at " + err.emaildomain);
rl.question("Steam Guard Code: ", function (code) {
doLogin(accountName, password, code);
});
return;
}
if(err.message == 'CAPTCHA') {
console.log(err.captchaurl);
rl.question("CAPTCHA: ", function(captchaInput) {
doLogin(accountName, password, authCode, captchaInput);
});
if (err.eresult == EResult.RateLimitExceeded) {
console.log('Error: RateLimitExceeded. Try again later.');
process.exit();
return;
}
@@ -51,57 +59,85 @@ function doLogin(accountName, password, authCode, captcha) {
return;
}
console.log("Logged on!");
community.enableTwoFactor(function(err, response) {
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!");
}
if (response.status != EResult.OK) {
console.log(`Error: Status ${response.status}`);
process.exit();
return;
}
let filename = `twofactor_${community.steamID.getSteamID64()}.json`;
console.log(`Writing secrets to ${filename}`);
console.log(`Revocation code: ${response.revocation_code}`);
FS.writeFileSync(filename, JSON.stringify(response, null, '\t'));
promptActivationCode(response);
});
}
async function promptActivationCode(response) {
if (response.phone_number_hint) {
console.log(`An activation code has been sent to your phone ending in ${response.phone_number_hint}.`);
} else if (response.confirm_type == 3) {
// Exact meaning of confirm_type is unknown, but 3 appears to be email code
console.log('An activation code has been sent to your email.');
}
let smsCode = await promptAsync('Activation 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');
}

426
index.js
View File

@@ -1,15 +1,19 @@
var Request = require('request');
var RSA = require('node-bignumber').Key;
var hex2b64 = require('node-bignumber').hex2b64;
var SteamID = require('steamid');
const {chrome} = require('@doctormckay/user-agents');
const Request = require('request');
const SteamID = require('steamid');
const USER_AGENT = "Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/47.0.2526.106 Safari/537.36";
const Helpers = require('./components/helpers.js');
require('util').inherits(SteamCommunity, require('events').EventEmitter);
module.exports = SteamCommunity;
SteamCommunity.SteamID = SteamID;
SteamCommunity.ConfirmationType = require('./resources/EConfirmationType.js');
SteamCommunity.EResult = require('./resources/EResult.js');
SteamCommunity.ESharedFileType = require('./resources/ESharedFileType.js');
SteamCommunity.EFriendRelationship = require('./resources/EFriendRelationship.js');
function SteamCommunity(options) {
options = options || {};
@@ -24,7 +28,7 @@ function SteamCommunity(options) {
"timeout": options.timeout || 50000,
"gzip": true,
"headers": {
"User-Agent": options.userAgent || USER_AGENT
"User-Agent": options.userAgent || chrome()
}
};
@@ -39,142 +43,45 @@ function SteamCommunity(options) {
defaults.localAddress = options.localAddress;
}
this.request = options.request || Request.defaults();
this.request = options.request || Request.defaults({"forever": true}); // "forever" indicates that we want a keep-alive agent
this.request = this.request.defaults(defaults);
// English
this._jar.setCookie(Request.cookie('Steam_Language=english'), 'https://steamcommunity.com');
this._setCookie(Request.cookie('Steam_Language=english'));
// UTC
this._jar.setCookie(Request.cookie('timezoneOffset=0,0'), 'https://steamcommunity.com');
this._setCookie(Request.cookie('timezoneOffset=0,0'));
}
SteamCommunity.prototype.login = function(details, callback) {
if(details.steamguard) {
var parts = details.steamguard.split('||');
this._jar.setCookie(Request.cookie('steamMachineAuth' + parts[0] + '=' + encodeURIComponent(parts[1])), 'https://steamcommunity.com');
if (!details.accountName || !details.password) {
throw new Error("Missing either accountName or password to login; both are needed");
}
var self = this;
// headers required to convince steam that we're logging in from a mobile device so that we can get the oAuth data
var mobileHeaders = {
"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, */*"
};
// Delete the cache
delete this._profileURL;
this._jar.setCookie(Request.cookie("mobileClientVersion=0 (2.1.3)"), "https://steamcommunity.com");
this._jar.setCookie(Request.cookie("mobileClient=android"), "https://steamcommunity.com");
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;
// default disableMobile to true
let logOnOptions = Object.assign({}, details);
logOnOptions.disableMobile = details.disableMobile !== false;
this._modernLogin(logOnOptions).then(({sessionID, cookies, steamguard, mobileAccessToken}) => {
this.setCookies(cookies);
if (mobileAccessToken) {
this.setMobileAppAccessToken(mobileAccessToken);
}
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._jar.setCookie(Request.cookie('sessionid=' + sessionID), 'http://steamcommunity.com');
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;
}
}
callback(null, sessionID, cookies, steamguard, oAuth.oauth_token);
}
}, "steamcommunity");
}, "steamcommunity");
function deleteMobileCookies() {
var cookie = Request.cookie('mobileClientVersion=');
cookie.expires = new Date(0);
self._jar.setCookie(cookie, "https://steamcommunity.com");
cookie = Request.cookie('mobileClient=');
cookie.expires = new Date(0);
self._jar.setCookie(cookie, "https://steamcommunity.com");
}
callback(null, sessionID, cookies, steamguard, null);
}).catch(err => callback(err));
};
/**
* @deprecated
* @param {string} steamguard
* @param {string} token
* @param {function} callback
*/
SteamCommunity.prototype.oAuthLogin = function(steamguard, token, callback) {
steamguard = steamguard.split('||');
var steamID = new SteamID(steamguard[0]);
@@ -209,29 +116,79 @@ SteamCommunity.prototype.oAuthLogin = function(steamguard, token, callback) {
}, "steamcommunity");
};
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]);
/**
* Get a token that can be used to log onto Steam using steam-user.
* @param {function} callback
*/
SteamCommunity.prototype.getClientLogonToken = function(callback) {
this.httpRequestGet({
"uri": "https://steamcommunity.com/chat/clientjstoken",
"json": true
}, (err, res, body) => {
if (err || res.statusCode != 200) {
callback(err ? err : new Error('HTTP error ' + res.statusCode));
return;
}
self._jar.setCookie(Request.cookie(cookie), (cookieName.match(/^steamMachineAuth/) || cookieName.match(/Secure$/) ? "https://" : "http://") + "steamcommunity.com");
if (!body.logged_in) {
let e = new Error('Not Logged In');
callback(e);
this._notifySessionExpired(e);
return;
}
if (!body.steamid || !body.account_name || !body.token) {
callback(new Error('Malformed response'));
return;
}
callback(null, {
"steamID": new SteamID(body.steamid),
"accountName": body.account_name,
"webLogonToken": body.token
});
});
};
SteamCommunity.prototype.getSessionID = function() {
var cookies = this._jar.getCookieString("http://steamcommunity.com").split(';');
SteamCommunity.prototype._setCookie = function(cookie, secure) {
var protocol = secure ? "https" : "http";
cookie.secure = !!secure;
if (cookie.domain) {
this._jar.setCookie(cookie.clone(), protocol + '://' + cookie.domain);
} else {
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) {
cookies.forEach((cookie) => {
var cookieName = cookie.trim().split('=')[0];
if (cookieName == 'steamLogin' || cookieName == 'steamLoginSecure') {
this.steamID = new SteamID(cookie.match(/steamLogin(Secure)?=(\d+)/)[2]);
}
this._setCookie(Request.cookie(cookie), !!(cookieName.match(/^steamMachineAuth/) || cookieName.match(/Secure$/)));
});
// 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(host = "http://steamcommunity.com") {
var cookies = this._jar.getCookieString(host).split(';');
for(var i = 0; i < cookies.length; i++) {
var match = cookies[i].trim().match(/([^=]+)=(.+)/);
if(match[1] == 'sessionid') {
return decodeURIComponent(match[2]);
}
}
var sessionID = generateSessionID();
this._jar.setCookie(Request.cookie('sessionid=' + sessionID), "http://steamcommunity.com");
this._setCookie(Request.cookie('sessionid=' + sessionID));
return sessionID;
};
@@ -241,69 +198,80 @@ function generateSessionID() {
SteamCommunity.prototype.parentalUnlock = function(pin, callback) {
var self = this;
var sessionID = self.getSessionID();
this.httpRequestPost("https://steamcommunity.com/parental/ajaxunlock", {
"json": true,
"form": {
"pin": pin
"pin": pin,
"sessionid": sessionID
}
}, function(err, response, body) {
if(!callback) {
return;
}
if (err) {
callback(err);
return;
}
if(!body || typeof body.success !== 'boolean') {
if (!body || typeof body.success !== 'boolean') {
callback("Invalid response");
return;
}
if(!body.success) {
callback("Incorrect PIN");
if (!body.success) {
switch (body.eresult) {
case 15:
callback("Incorrect PIN");
break;
case 25:
callback("Too many invalid PIN attempts");
break;
default:
callback("Error " + body.eresult);
}
return;
}
callback();
}.bind(this), "steamcommunity");
};
SteamCommunity.prototype.getNotifications = function(callback) {
var self = this;
this.httpRequestGet("https://steamcommunity.com/actions/RefreshNotificationArea", function(err, response, body) {
this.httpRequestGet({
"uri": "https://steamcommunity.com/actions/GetNotificationCounts",
"json": true
}, function(err, response, body) {
if (err) {
callback(err);
return;
}
var notifications = {
"comments": 0,
"items": 0,
"invites": 0,
"gifts": 0,
"chat": 0,
"trades": 0
};
var items = {
"comments": /(\d+) new comments?/,
"items": /(\d+) new items? in your inventory/,
"invites": /(\d+) new invites?/,
"gifts": /(\d+) new gifts?/,
"chat": /(\d+) unread chat messages?/,
"trades": /(\d+) new trade notifications?/
};
var match;
for(var i in items) {
if(match = body.match(items[i])) {
notifications[i] = parseInt(match[1], 10);
}
if (!body || !body.notifications) {
callback(new Error("Malformed response"));
return;
}
var notifications = {
"trades": body.notifications[1] || 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
"gifts": body.notifications[8] || 0,
"chat": body.notifications[9] || 0,
"helpRequestReplies": body.notifications[10] || 0,
"accountAlerts": body.notifications[11] || 0
};
callback(null, notifications);
}, "steamcommunity");
};
@@ -329,16 +297,76 @@ SteamCommunity.prototype.loggedIn = function(callback) {
callback(err || new Error("HTTP error " + response.statusCode));
return;
}
if(response.statusCode == 403) {
callback(null, true, true);
return;
}
callback(null, !!response.headers.location.match(/steamcommunity\.com(\/(id|profiles)\/[^\/]+)\/?/), false);
}, "steamcommunity");
};
SteamCommunity.prototype.getTradeURL = function(callback) {
this._myProfile("tradeoffers/privacy", null, (err, response, body) => {
if (err) {
callback(err);
return;
}
var match = body.match(/https?:\/\/(www.)?steamcommunity.com\/tradeoffer\/new\/?\?partner=\d+(&|&amp;)token=([a-zA-Z0-9-_]+)/);
if (match) {
var token = match[3];
callback(null, match[0], token);
} else {
callback(new Error("Malformed response"));
}
}, "steamcommunity");
};
SteamCommunity.prototype.changeTradeURL = function(callback) {
this._myProfile("tradeoffers/newtradeurl", {"sessionid": this.getSessionID()}, (err, response, body) => {
if (!callback) {
return;
}
if (!body || typeof body !== "string" || body.length < 3 || body.indexOf('"') !== 0) {
callback(new Error("Malformed response"));
return;
}
var newToken = body.replace(/"/g, ''); //"t1o2k3e4n" => t1o2k3e4n
callback(null, "https://steamcommunity.com/tradeoffer/new/?partner=" + this.steamID.accountid + "&token=" + newToken, newToken);
}, "steamcommunity");
};
/**
* Clear your profile name (alias) history.
* @param {function} callback
*/
SteamCommunity.prototype.clearPersonaNameHistory = function(callback) {
this._myProfile("ajaxclearaliashistory/", {"sessionid": this.getSessionID()}, (err, res, body) => {
if (!callback) {
return;
}
if (err) {
return callback(err);
}
if (res.statusCode != 200) {
return callback(new Error("HTTP error " + res.statusCode));
}
try {
body = JSON.parse(body);
callback(Helpers.eresultError(body.success));
} catch (ex) {
return callback(new Error("Malformed response"));
}
});
};
SteamCommunity.prototype._myProfile = function(endpoint, form, callback) {
var self = this;
@@ -360,43 +388,79 @@ SteamCommunity.prototype._myProfile = function(endpoint, form, callback) {
self._profileURL = match[1];
setTimeout(function () {
delete self._profileURL; // delete the cache
}, 60000);
}, 60000).unref();
completeRequest(match[1]);
}, "steamcommunity");
}
function completeRequest(url) {
var options = {
"uri": "https://steamcommunity.com" + url + "/" + endpoint,
"method": "GET"
};
var options = endpoint.endpoint ? endpoint : {};
options.uri = "https://steamcommunity.com" + url + "/" + (endpoint.endpoint || endpoint);
if (form) {
options.method = "POST";
options.form = form;
options.followAllRedirects = true;
} else if (!options.method) {
options.method = "GET";
}
self.httpRequest(options, callback, "steamcommunity");
}
};
/**
* 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
*/
SteamCommunity.prototype.getFriendsList = function(callback) {
this.httpRequestGet({
"uri": "https://steamcommunity.com/textfilter/ajaxgetfriendslist",
"json": true
}, (err, res, body) => {
if (err) {
callback(err ? err : new Error('HTTP error ' + res.statusCode));
return;
}
if (body.success != 1) {
callback(Helpers.eresultError(body.success));
return;
}
if (!body.friendslist || !body.friendslist.friends) {
callback(new Error('Malformed response'));
return;
}
const friends = {};
body.friendslist.friends.forEach(friend => (friends[friend.ulfriendid] = friend.efriendrelationship));
callback(null, friends);
});
};
require('./components/login.js');
require('./components/http.js');
require('./components/chat.js');
require('./components/profile.js');
require('./components/market.js');
require('./components/groups.js');
require('./components/users.js');
require('./components/sharedfiles.js');
require('./components/inventoryhistory.js');
require('./components/webapi.js');
require('./components/twofactor.js');
require('./components/confirmations.js');
require('./components/help.js');
require('./classes/CMarketItem.js');
require('./classes/CMarketSearchResult.js');
require('./classes/CSteamGroup.js');
require('./classes/CSteamSharedFile.js');
require('./classes/CSteamUser.js');
/**
@callback SteamCommunity~genericErrorCallback
@param {Error|null} err - An Error object on failure, or null on success
*/
*/

View File

@@ -1,25 +1,42 @@
{
"name": "steamcommunity",
"version": "3.23.1",
"version": "3.50.0",
"description": "Provides an interface for logging into and interacting with the Steam Community website",
"keywords": ["steam", "steam community"],
"files": [
"/classes",
"/components",
"/examples",
"/resources",
"/index.js"
],
"keywords": [
"steam",
"steam community"
],
"homepage": "https://github.com/DoctorMcKay/node-steamcommunity",
"bugs": {
"url": "https://github.com/DoctorMcKay/node-steamcommunity/issues"
},
"license": "MIT",
"author": {
"name": "Alex Corn",
"email": "mckay@doctormckay.com",
"url": "https://www.doctormckay.com"
},
"repository": {
"type": "git",
"url": "https://github.com/DoctorMcKay/node-steamcommunity.git"
},
"dependencies": {
"request": "^2.61.0",
"node-bignumber": "^1.2.1",
"steamid": "^1.0.0",
"xml2js": "^0.4.11",
"cheerio": "0.19.0",
"async": "^1.4.2",
"steam-totp": "^1.3.0"
"@doctormckay/user-agents": "^1.0.0",
"async": "^2.6.3",
"cheerio": "0.22.0",
"image-size": "^0.8.2",
"request": "^2.88.0",
"steam-session": "^1.9.1",
"steam-totp": "^1.5.0",
"steamid": "^1.1.3",
"xml2js": "^0.6.2"
},
"engines": {
"node": ">=4.0.0"

14
resources/EChatState.js Normal file
View File

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

View File

@@ -0,0 +1,12 @@
/**
* @enum EConfirmationType
*/
module.exports = {
// 1 is unknown, possibly "Invalid"
"Trade": 2,
"MarketListing": 3,
// 4 is opt-out or other like account confirmation?
"2": "Trade",
"3": "MarketListing"
};

View File

@@ -0,0 +1,23 @@
/**
* @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

@@ -0,0 +1,23 @@
/**
* @enum EPersonaState
*/
module.exports = {
"Offline": 0,
"Online": 1,
"Busy": 2,
"Away": 3,
"Snooze": 4,
"LookingToTrade": 5,
"LookingToPlay": 6,
"Invisible": 7,
// Value-to-name mapping for convenience
"0": "Offline",
"1": "Online",
"2": "Busy",
"3": "Away",
"4": "Snooze",
"5": "LookingToTrade",
"6": "LookingToPlay",
"7": "Invisible",
};

View File

@@ -0,0 +1,32 @@
/**
* @enum EPersonaStateFlag
*/
module.exports = {
"HasRichPresence": 1,
"InJoinableGame": 2,
"Golden": 4,
"RemotePlayTogether": 8,
"OnlineUsingWeb": 256, // removed "renamed to ClientTypeWeb"
"ClientTypeWeb": 256,
"OnlineUsingMobile": 512, // removed "renamed to ClientTypeMobile"
"ClientTypeMobile": 512,
"OnlineUsingBigPicture": 1024, // removed "renamed to ClientTypeTenfoot"
"ClientTypeTenfoot": 1024,
"OnlineUsingVR": 2048, // removed "renamed to ClientTypeVR"
"ClientTypeVR": 2048,
"LaunchTypeGamepad": 4096,
"LaunchTypeCompatTool": 8192,
// Value-to-name mapping for convenience
"1": "HasRichPresence",
"2": "InJoinableGame",
"4": "Golden",
"8": "RemotePlayTogether",
"256": "ClientTypeWeb",
"512": "ClientTypeMobile",
"1024": "ClientTypeTenfoot",
"2048": "ClientTypeVR",
"4096": "LaunchTypeGamepad",
"8192": "LaunchTypeCompatTool",
};

254
resources/EResult.js Normal file
View File

@@ -0,0 +1,254 @@
/**
* @enum EResult
*/
module.exports = {
"Invalid": 0,
"OK": 1,
"Fail": 2,
"NoConnection": 3,
"InvalidPassword": 5,
"LoggedInElsewhere": 6,
"InvalidProtocolVer": 7,
"InvalidParam": 8,
"FileNotFound": 9,
"Busy": 10,
"InvalidState": 11,
"InvalidName": 12,
"InvalidEmail": 13,
"DuplicateName": 14,
"AccessDenied": 15,
"Timeout": 16,
"Banned": 17,
"AccountNotFound": 18,
"InvalidSteamID": 19,
"ServiceUnavailable": 20,
"NotLoggedOn": 21,
"Pending": 22,
"EncryptionFailure": 23,
"InsufficientPrivilege": 24,
"LimitExceeded": 25,
"Revoked": 26,
"Expired": 27,
"AlreadyRedeemed": 28,
"DuplicateRequest": 29,
"AlreadyOwned": 30,
"IPNotFound": 31,
"PersistFailed": 32,
"LockingFailed": 33,
"LogonSessionReplaced": 34,
"ConnectFailed": 35,
"HandshakeFailed": 36,
"IOFailure": 37,
"RemoteDisconnect": 38,
"ShoppingCartNotFound": 39,
"Blocked": 40,
"Ignored": 41,
"NoMatch": 42,
"AccountDisabled": 43,
"ServiceReadOnly": 44,
"AccountNotFeatured": 45,
"AdministratorOK": 46,
"ContentVersion": 47,
"TryAnotherCM": 48,
"PasswordRequiredToKickSession": 49,
"AlreadyLoggedInElsewhere": 50,
"Suspended": 51,
"Cancelled": 52,
"DataCorruption": 53,
"DiskFull": 54,
"RemoteCallFailed": 55,
"PasswordNotSet": 56, // removed "renamed to PasswordUnset"
"PasswordUnset": 56,
"ExternalAccountUnlinked": 57,
"PSNTicketInvalid": 58,
"ExternalAccountAlreadyLinked": 59,
"RemoteFileConflict": 60,
"IllegalPassword": 61,
"SameAsPreviousValue": 62,
"AccountLogonDenied": 63,
"CannotUseOldPassword": 64,
"InvalidLoginAuthCode": 65,
"AccountLogonDeniedNoMailSent": 66, // removed "renamed to AccountLogonDeniedNoMail"
"AccountLogonDeniedNoMail": 66,
"HardwareNotCapableOfIPT": 67,
"IPTInitError": 68,
"ParentalControlRestricted": 69,
"FacebookQueryError": 70,
"ExpiredLoginAuthCode": 71,
"IPLoginRestrictionFailed": 72,
"AccountLocked": 73, // removed "renamed to AccountLockedDown"
"AccountLockedDown": 73,
"AccountLogonDeniedVerifiedEmailRequired": 74,
"NoMatchingURL": 75,
"BadResponse": 76,
"RequirePasswordReEntry": 77,
"ValueOutOfRange": 78,
"UnexpectedError": 79,
"Disabled": 80,
"InvalidCEGSubmission": 81,
"RestrictedDevice": 82,
"RegionLocked": 83,
"RateLimitExceeded": 84,
"AccountLogonDeniedNeedTwoFactorCode": 85, // removed "renamed to AccountLoginDeniedNeedTwoFactor"
"AccountLoginDeniedNeedTwoFactor": 85,
"ItemOrEntryHasBeenDeleted": 86, // removed "renamed to ItemDeleted"
"ItemDeleted": 86,
"AccountLoginDeniedThrottle": 87,
"TwoFactorCodeMismatch": 88,
"TwoFactorActivationCodeMismatch": 89,
"AccountAssociatedToMultiplePlayers": 90, // removed "renamed to AccountAssociatedToMultiplePartners"
"AccountAssociatedToMultiplePartners": 90,
"NotModified": 91,
"NoMobileDeviceAvailable": 92, // removed "renamed to NoMobileDevice"
"NoMobileDevice": 92,
"TimeIsOutOfSync": 93, // removed "renamed to TimeNotSynced"
"TimeNotSynced": 93,
"SMSCodeFailed": 94,
"TooManyAccountsAccessThisResource": 95, // removed "renamed to AccountLimitExceeded"
"AccountLimitExceeded": 95,
"AccountActivityLimitExceeded": 96,
"PhoneActivityLimitExceeded": 97,
"RefundToWallet": 98,
"EmailSendFailure": 99,
"NotSettled": 100,
"NeedCaptcha": 101,
"GSLTDenied": 102,
"GSOwnerDenied": 103,
"InvalidItemType": 104,
"IPBanned": 105,
"GSLTExpired": 106,
"InsufficientFunds": 107,
"TooManyPending": 108,
"NoSiteLicensesFound": 109,
"WGNetworkSendExceeded": 110,
"AccountNotFriends": 111,
"LimitedUserAccount": 112,
"CantRemoveItem": 113,
"AccountHasBeenDeleted": 114,
"AccountHasAnExistingUserCancelledLicense": 115,
"DeniedDueToCommunityCooldown": 116,
"NoLauncherSpecified": 117,
"MustAgreeToSSA": 118,
"ClientNoLongerSupported": 119,
// Value-to-name mapping for convenience
"0": "Invalid",
"1": "OK",
"2": "Fail",
"3": "NoConnection",
"5": "InvalidPassword",
"6": "LoggedInElsewhere",
"7": "InvalidProtocolVer",
"8": "InvalidParam",
"9": "FileNotFound",
"10": "Busy",
"11": "InvalidState",
"12": "InvalidName",
"13": "InvalidEmail",
"14": "DuplicateName",
"15": "AccessDenied",
"16": "Timeout",
"17": "Banned",
"18": "AccountNotFound",
"19": "InvalidSteamID",
"20": "ServiceUnavailable",
"21": "NotLoggedOn",
"22": "Pending",
"23": "EncryptionFailure",
"24": "InsufficientPrivilege",
"25": "LimitExceeded",
"26": "Revoked",
"27": "Expired",
"28": "AlreadyRedeemed",
"29": "DuplicateRequest",
"30": "AlreadyOwned",
"31": "IPNotFound",
"32": "PersistFailed",
"33": "LockingFailed",
"34": "LogonSessionReplaced",
"35": "ConnectFailed",
"36": "HandshakeFailed",
"37": "IOFailure",
"38": "RemoteDisconnect",
"39": "ShoppingCartNotFound",
"40": "Blocked",
"41": "Ignored",
"42": "NoMatch",
"43": "AccountDisabled",
"44": "ServiceReadOnly",
"45": "AccountNotFeatured",
"46": "AdministratorOK",
"47": "ContentVersion",
"48": "TryAnotherCM",
"49": "PasswordRequiredToKickSession",
"50": "AlreadyLoggedInElsewhere",
"51": "Suspended",
"52": "Cancelled",
"53": "DataCorruption",
"54": "DiskFull",
"55": "RemoteCallFailed",
"56": "PasswordUnset",
"57": "ExternalAccountUnlinked",
"58": "PSNTicketInvalid",
"59": "ExternalAccountAlreadyLinked",
"60": "RemoteFileConflict",
"61": "IllegalPassword",
"62": "SameAsPreviousValue",
"63": "AccountLogonDenied",
"64": "CannotUseOldPassword",
"65": "InvalidLoginAuthCode",
"66": "AccountLogonDeniedNoMail",
"67": "HardwareNotCapableOfIPT",
"68": "IPTInitError",
"69": "ParentalControlRestricted",
"70": "FacebookQueryError",
"71": "ExpiredLoginAuthCode",
"72": "IPLoginRestrictionFailed",
"73": "AccountLockedDown",
"74": "AccountLogonDeniedVerifiedEmailRequired",
"75": "NoMatchingURL",
"76": "BadResponse",
"77": "RequirePasswordReEntry",
"78": "ValueOutOfRange",
"79": "UnexpectedError",
"80": "Disabled",
"81": "InvalidCEGSubmission",
"82": "RestrictedDevice",
"83": "RegionLocked",
"84": "RateLimitExceeded",
"85": "AccountLoginDeniedNeedTwoFactor",
"86": "ItemDeleted",
"87": "AccountLoginDeniedThrottle",
"88": "TwoFactorCodeMismatch",
"89": "TwoFactorActivationCodeMismatch",
"90": "AccountAssociatedToMultiplePartners",
"91": "NotModified",
"92": "NoMobileDevice",
"93": "TimeNotSynced",
"94": "SMSCodeFailed",
"95": "AccountLimitExceeded",
"96": "AccountActivityLimitExceeded",
"97": "PhoneActivityLimitExceeded",
"98": "RefundToWallet",
"99": "EmailSendFailure",
"100": "NotSettled",
"101": "NeedCaptcha",
"102": "GSLTDenied",
"103": "GSOwnerDenied",
"104": "InvalidItemType",
"105": "IPBanned",
"106": "GSLTExpired",
"107": "InsufficientFunds",
"108": "TooManyPending",
"109": "NoSiteLicensesFound",
"110": "WGNetworkSendExceeded",
"111": "AccountNotFriends",
"112": "LimitedUserAccount",
"113": "CantRemoveItem",
"114": "AccountHasBeenDeleted",
"115": "AccountHasAnExistingUserCancelledLicense",
"116": "DeniedDueToCommunityCooldown",
"117": "NoLauncherSpecified",
"118": "MustAgreeToSSA",
"119": "ClientNoLongerSupported",
};

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