diff --git a/.npmrc b/.npmrc new file mode 100644 index 0000000..43c97e7 --- /dev/null +++ b/.npmrc @@ -0,0 +1 @@ +package-lock=false diff --git a/README.md b/README.md index a569dc6..175a289 100644 --- a/README.md +++ b/README.md @@ -9,6 +9,8 @@ This module provides an easy interface for the Steam Community website. This mod It supports Steam Guard and CAPTCHAs. +This reports anonymous usage statistics to the author. [See here](https://github.com/DoctorMcKay/node-stats-reporter) for more information. + **Have a question about the module or coding in general? *Do not create a GitHub issue.* GitHub issues are for feature requests and bug reports. Instead, post in the [dedicated forum](https://dev.doctormckay.com/forum/8-node-steamcommunity/). Such issues may be ignored!** diff --git a/classes/CSteamGroup.js b/classes/CSteamGroup.js index 680ed4c..9988d57 100644 --- a/classes/CSteamGroup.js +++ b/classes/CSteamGroup.js @@ -114,10 +114,37 @@ 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); + +/** + * 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); }; diff --git a/components/chat.js b/components/chat.js index 252fa12..21e743a 100644 --- a/components/chat.js +++ b/components/chat.js @@ -1,32 +1,9 @@ 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'); SteamCommunity.prototype.chatLogon = function(interval, uiMode) { if(this.chatState == SteamCommunity.ChatState.LoggingOn || this.chatState == SteamCommunity.ChatState.LoggedOn) { @@ -181,7 +158,7 @@ SteamCommunity.prototype._chatPoll = function() { }, "json": true }, function(err, response, body) { - if(self.chatState == SteamCommunity.ChatState.Offline) { + if (self.chatState == SteamCommunity.ChatState.Offline) { return; } @@ -242,12 +219,20 @@ SteamCommunity.prototype._relogWebChat = function() { }; 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() { diff --git a/components/confirmations.js b/components/confirmations.js index e355719..66770cd 100644 --- a/components/confirmations.js +++ b/components/confirmations.js @@ -243,6 +243,10 @@ 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(); diff --git a/components/groups.js b/components/groups.js index 3405416..3536c94 100644 --- a/components/groups.js +++ b/components/groups.js @@ -3,6 +3,7 @@ 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 || []; @@ -153,7 +154,7 @@ SteamCommunity.prototype.getAllGroupAnnouncements = function(gid, time, callback return callback(null, announcements); }); }, "steamcommunity"); -} +}; SteamCommunity.prototype.postGroupAnnouncement = function(gid, headline, content, callback) { if(typeof gid === 'string') { @@ -581,4 +582,109 @@ SteamCommunity.prototype.deleteGroupComment = function(gid, cid, callback) { callback(err || null); }, "steamcommunity"); -}; \ No newline at end of file +}; + +/** + * 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"); +}; diff --git a/components/users.js b/components/users.js index 2d91aef..b65a9eb 100644 --- a/components/users.js +++ b/components/users.js @@ -218,16 +218,16 @@ SteamCommunity.prototype.getUserAliases = function(userID, callback) { }; SteamCommunity.prototype.getUserInventoryContexts = function(userID, callback) { - if(typeof userID === 'string') { + if (typeof userID === 'string') { userID = new SteamID(userID); } - if(typeof userID === 'function') { + 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 +240,19 @@ 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) { + var errorMessage = "Malformed response"; + + if(body.match(/0 items in their inventory\./)){ + callback(null, {}); + return; + }else if(body.match(/inventory is currently private\./)){ + errorMessage = "Private inventory"; + }else if(body.match(/profile\_private\_info/)){ + errorMessage = "Private profile"; + } + + callback(new Error(errorMessage)); return; } @@ -340,15 +351,21 @@ SteamCommunity.prototype.getUserInventory = function(userID, appID, contextID, t * @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, callback) { +SteamCommunity.prototype.getUserInventoryContents = function(userID, appID, contextID, tradableOnly, language, callback) { var self = this; - if(typeof userID === 'string') { + if (typeof userID === 'string') { userID = new SteamID(userID); } + if (typeof language === 'function') { + callback = language; + language = "english"; + } + var pos = 1; get([], []); @@ -359,7 +376,7 @@ SteamCommunity.prototype.getUserInventoryContents = function(userID, appID, cont "Referer": "https://steamcommunity.com/profiles/" + userID.getSteamID64() + "/inventory" }, "qs": { - "l": "english", // Default language + "l": language, // Default language "count": 5000, // Max items per 'page' "start_assetid": start }, @@ -368,7 +385,7 @@ SteamCommunity.prototype.getUserInventoryContents = function(userID, appID, cont if (err) { if (err.message == "HTTP error 403" && body === null) { // 403 with a body of "null" means the inventory/profile is private. - if(userID.getSteamID64() == self.steamID.getSteamID64()) { + if (self.steamID && userID.getSteamID64() == self.steamID.getSteamID64()) { // We can never get private profile error for our own inventory! self._notifySessionExpired(err); } @@ -431,20 +448,16 @@ SteamCommunity.prototype.getUserInventoryContents = function(userID, appID, cont var quickDescriptionLookup = {}; function getDescription(descriptions, classID, instanceID) { - instanceID = instanceID || '0'; // instanceID can be undefined, in which case it's 0. - - var key = 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[key] = descriptions[i]; - - if (descriptions[i].classid == classID && descriptions[i].instanceid == instanceID) { - return descriptions[i]; - } + quickDescriptionLookup[descriptions[i].classid + '_' + (descriptions[i].instanceid || '0')] = descriptions[i]; } + + return quickDescriptionLookup[key]; } }; diff --git a/index.js b/index.js index 46a19b3..a802983 100644 --- a/index.js +++ b/index.js @@ -1,3 +1,5 @@ +require('@doctormckay/stats-reporter').setup(require('./package.json')); + var Request = require('request'); var RSA = require('node-bignumber').Key; var hex2b64 = require('node-bignumber').hex2b64; @@ -10,12 +12,8 @@ require('util').inherits(SteamCommunity, require('events').EventEmitter); module.exports = SteamCommunity; SteamCommunity.SteamID = SteamID; -SteamCommunity.ConfirmationType = { - // 1 is unknown, possibly "Invalid" - "Trade": 2, - "MarketListing": 3 - // 4 is opt-out or other like account confirmation? -}; +SteamCommunity.ConfirmationType = require('./resources/EConfirmationType.js'); +SteamCommunity.EResult = require('./resources/EResult.js'); function SteamCommunity(options) { options = options || {}; @@ -281,13 +279,25 @@ SteamCommunity.prototype.parentalUnlock = function(pin, callback) { 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; } diff --git a/package.json b/package.json index 5ec5379..426b959 100644 --- a/package.json +++ b/package.json @@ -1,8 +1,11 @@ { "name": "steamcommunity", - "version": "3.30.6", + "version": "3.32.2", "description": "Provides an interface for logging into and interacting with the Steam Community website", - "keywords": ["steam", "steam community"], + "keywords": [ + "steam", + "steam community" + ], "homepage": "https://github.com/DoctorMcKay/node-steamcommunity", "bugs": { "url": "https://github.com/DoctorMcKay/node-steamcommunity/issues" @@ -13,7 +16,8 @@ "url": "https://github.com/DoctorMcKay/node-steamcommunity.git" }, "dependencies": { - "request": "^2.61.0", + "@doctormckay/stats-reporter": "^1.0.2", + "request": "^2.81.0", "node-bignumber": "^1.2.1", "steamid": "^1.0.0", "xml2js": "^0.4.11", diff --git a/resources/EChatState.js b/resources/EChatState.js new file mode 100644 index 0000000..287ee8d --- /dev/null +++ b/resources/EChatState.js @@ -0,0 +1,14 @@ +/** + * @enum EChatState + */ +module.exports = { + "Offline": 0, + "LoggingOn": 1, + "LogOnFailed": 2, + "LoggedOn": 3, + + "0": "Offline", + "1": "LoggingOn", + "2": "LogOnFailed", + "3": "LoggedOn" +}; diff --git a/resources/EConfirmationType.js b/resources/EConfirmationType.js new file mode 100644 index 0000000..3c92474 --- /dev/null +++ b/resources/EConfirmationType.js @@ -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" +}; diff --git a/resources/EPersonaState.js b/resources/EPersonaState.js new file mode 100644 index 0000000..2bb811a --- /dev/null +++ b/resources/EPersonaState.js @@ -0,0 +1,23 @@ +/** + * @enum EPersonaState + */ +module.exports = { + "Offline": 0, + "Online": 1, + "Busy": 2, + "Away": 3, + "Snooze": 4, + "LookingToTrade": 5, + "LookingToPlay": 6, + "Max": 7, + + // Value-to-name mapping for convenience + "0": "Offline", + "1": "Online", + "2": "Busy", + "3": "Away", + "4": "Snooze", + "5": "LookingToTrade", + "6": "LookingToPlay", + "7": "Max", +}; diff --git a/resources/EPersonaStateFlag.js b/resources/EPersonaStateFlag.js new file mode 100644 index 0000000..dfb7b81 --- /dev/null +++ b/resources/EPersonaStateFlag.js @@ -0,0 +1,27 @@ +/** + * @enum EPersonaStateFlag + */ +module.exports = { + "HasRichPresence": 1, + "InJoinableGame": 2, + "Golden": 4, // removed "no longer has any effect" + "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, + + // Value-to-name mapping for convenience + "1": "HasRichPresence", + "2": "InJoinableGame", + "4": "Golden", + "256": "ClientTypeWeb", + "512": "ClientTypeMobile", + "1024": "ClientTypeTenfoot", + "2048": "ClientTypeVR", + "4096": "LaunchTypeGamepad", +}; diff --git a/resources/EResult.js b/resources/EResult.js new file mode 100644 index 0000000..0ebdc2c --- /dev/null +++ b/resources/EResult.js @@ -0,0 +1,232 @@ +/** + * @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, + + // 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", +};