├── .gitignore ├── LICENSE ├── README.md ├── index.js ├── package.json └── psn_request.js /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | test* 3 | .psnAuth* -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2014 José Augusto Sächs 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | PSNjs 2 | ========= 3 | 4 | ##About 5 | 6 | This is a simple Node.JS API for accessing PSN data. 7 | 8 | This is heavily based on work by psnapi.org and gumer-psn. 9 | 10 | Note: v0.1.x is a completely new API that is incompatible with 0.0.x. Please take care when writing your dependancies. 11 | 12 | ##Installing 13 | 14 | You can install it with the package manager 15 | 16 | npm install PSNjs 17 | 18 | Or clone the repository and install the dependencies 19 | 20 | git clone https://github.com/cubehouse/PSNjs.git 21 | cd PSNjs/ 22 | npm install 23 | 24 | ##API Setup 25 | ```javascript 26 | var PSNjs = require('PSNjs'); 27 | 28 | var psn = new PSNjs({ 29 | // PSN email and password 30 | email: "PSNEMAIL", 31 | password: "PSNPASSWORD", 32 | // debug printing 33 | debug: true, 34 | // optionally store session tokens in a file to speed up future connection 35 | authfile: ".psnAuth" 36 | }); 37 | 38 | // get the above user's trophies 39 | psn.getUserTrophies(function(error, data) { 40 | // check for an error 41 | if (error) 42 | { 43 | console.log("Error fetching trophies: " + error); 44 | return; 45 | } 46 | 47 | // success! print out trophy data 48 | console.log(JSON.stringify(data, null, 2)); 49 | }); 50 | ``` 51 | 52 | ###Other Init variables 53 | ```javascript 54 | { 55 | email: "PSNEMAIL", // your email 56 | password: "PSNPASSWORD", // your password 57 | debug: true, // enable debug logging? 58 | requestDebug: false, // enable the request library's debug output? 59 | autoconnect: false, // make a PSN request immediately (make sure you use onReady if you do this) 60 | authfile: ".psnAuth", // optionally store PSN session tokens in this file 61 | onReady: function() { 62 | // this function will be called when the API is ready (mainly used when autoconnect is true) 63 | } 64 | } 65 | ``` 66 | 67 | ##Custom Save and Load Callbacks 68 | 69 | If you don't want to use the authfile option, you can manually write save/load functions. For example, using Redis or something instead of the filesystem. 70 | 71 | ```javascript 72 | var psn = new PSNjs({ 73 | // PSN email and password 74 | email: "PSNEMAIL", 75 | password: "PSNPASSWORD" 76 | }); 77 | 78 | // load example (data should be a Base64 string) 79 | psn.Load("SAVED DATA", function(error) { 80 | if (error) 81 | { 82 | console.log("Error loading data: " + error); 83 | return; 84 | } 85 | 86 | // load successful! 87 | }); 88 | 89 | // save example 90 | psn.OnSave(function(data, callback) { 91 | // save data 92 | // data will be a Base64 string 93 | mySaveSystem.save(data, function() { 94 | // all done! 95 | // always call the callback so the API knows you're done saving! 96 | // handle your own error reporting and debugging 97 | callback(); 98 | }); 99 | }); 100 | ``` 101 | 102 | Functions 103 | ========= 104 | 105 | ## getProfile(username, callback) 106 | 107 | Get a PSN user's profile 108 | * username - PSN username to request 109 | * callback - Callback function with error (false if no error) and returned data object 110 | 111 | ## getMessageGroups(callback) 112 | 113 | Get current user's message groups 114 | * callback - Callback function with error (false if no error) and returned data object 115 | 116 | 117 | ## getMessageContent(messageGroupId, messageUid, messageKind, callback) 118 | 119 | Get data from a specific message. All this data can be found in getMessageGroups 120 | * messageGroupId - Group ID requested message belongs to 121 | * messageUid - Message ID to fetch 122 | * messageKind - Kind of message (as int) 123 | * callback - Callback function with error (false if no error) and returned data object 124 | 125 | ## getLatestActivity(feed, filters, page, callback) 126 | 127 | Get the signed-in user's activity feed 128 | * feed - type of feed, either "feed" or "news" (optional, defaults to "news") 129 | * filters - array of strings to filter by (optional, defaults to no filters) 130 | * PURCHASE, RATED, PLAYED_WITH, VIDEO_UPLOAD, SCREENSHOT_UPLOAD, PLAYED_GAME, LAUNCHED_GAME_FIRST_TIME, WATCHED_VIDEO, TROPHY, BROADCASTING, LIKED, PROFILE_ABOUT_ME, PROFILE_PIC, FRIENDED, CONTENT_SHARE, STORE_PROMO, IN_GAME_POST, 131 | * Use an empty array (or leave out argument) for all types 132 | * page - Page of feed to load (default: 0) 133 | * callback - Callback function with error (false if no error) and returned data object 134 | 135 | ## likeActivity(storyId, dislike, callback) 136 | 137 | Like an activity from the activity feed 138 | * storyId - The ID of the activity we want to like 139 | * dislike - (optional) set to true to dislike instead of like 140 | * callback - Callback function with error (false if no error) and returned data object 141 | 142 | ## dislikeActivity(storyId, callback) 143 | 144 | Dislike an activity from the activity feed 145 | * storyId - The ID of the activity we want to dislike 146 | * callback - Callback function with error (false if no error) and returned data object 147 | 148 | ## getNotifications(callback) 149 | 150 | Get notifications of currently authenticated user 151 | * callback - Callback function with error (false if no error) and returned data object 152 | 153 | ## addFriend(username, callback) 154 | 155 | Add a friend to PSN (must have received a friend request from the user) 156 | * username - Username to add 157 | * callback - Callback function with error (false if no error) and returned data object 158 | 159 | ## removeFriend(username, callback) 160 | 161 | Remove a friend from PSN 162 | * username Username to remove 163 | * callback - Callback function with error (false if no error) and returned data object 164 | 165 | ## sendFriendRequest(username, message, callback) 166 | 167 | Send a friend request to a user 168 | * username - Username to add 169 | * message - Message to send to user 170 | * callback - Callback function with error (false if no error) and returned data object 171 | 172 | ## getFriends(offset, limit, friendType, callback) 173 | 174 | Get the user's friend list 175 | * offset - (optional) Index to start friend list 176 | * limit - (optional) Maximum limit of friends to fetch 177 | * friendType - (optional) Type of friends to filter by (accepts friend, requesting or requested) 178 | * callback - Callback function with error (false if no error) and returned data object 179 | 180 | ## generateFriendURL(callback) 181 | 182 | Generate a friend URL you can give to people to add you as a friend. 183 | * callback - Callback function with error (false if no error) and returned data object 184 | 185 | ## getUserTrophies(offset, limit, username, callback) 186 | 187 | Fetch trophy data for the logged in user (and optionally compare to another user) 188 | * offset - (optional) Starting index of trophy data 189 | * limit - (optional) Maximum number of titles to fetch 190 | * username - (optional) PSN ID to compare trophies with 191 | * callback - Callback function with error (false if no error) and returned data object 192 | 193 | ## getTrophyGroups(npCommunicationId, username, callback) 194 | 195 | Get list of trophy groups for a title (eg. base game + DLC packs) 196 | * npCommunicationId - Title ID 197 | * Username - (optional) Username to compare trophies to 198 | * callback - Callback function with error (false if no error) and returned data object 199 | 200 | ## getTrophies(npCommunicationId, trophyGroupId, username, callback) 201 | 202 | Get a title's trophies (supplying a trophy group), optionally comparing to another user. 203 | * npCommunicationId - Title ID 204 | * trophyGroupId - Trophy Group ID (from getTrophyGroups) 205 | * Username - (optional) Username to compare trophies to 206 | * callback - Callback function with error (false if no error) and returned data object 207 | 208 | ## getTrophy(npCommunicationId, trophyGroupId, trophyId, username, callback) 209 | 210 | Get data on a specific trophy in a title with supplied trophyId. Optionally compare to a username. 211 | * npCommunicationId - Title ID 212 | * trophyGroupId - Trophy Group ID (from getTrophyGroups) 213 | * trophyId - Trophy ID 214 | * Username - (optional) Username to compare trophies to 215 | * callback - Callback function with error (false if no error) and returned data object 216 | 217 | 218 | 219 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | // load PSNRequest object (we will extend it with our helper functions) 2 | var PSNRequest = require("./psn_request"); 3 | 4 | // which fields to request when getting a profile 5 | var profileFields = "@default,relation,requestMessageFlag,presence,@personalDetail,trophySummary"; 6 | var friendFields = "@default,relation,onlineId,avatarUrl,plus,@personalDetail,trophySummary"; 7 | var messageFields = "@default,messageGroupId,messageGroupDetail,totalUnseenMessages,totalMessages,latestMessage"; 8 | var notificationFields = "@default,message,actionUrl"; 9 | var trophyFields = "@default,trophyRare,trophyEarnedRate"; 10 | 11 | /** Get a PSN user's profile 12 | * @param username PSN username to request 13 | * @param callback Callback function with error (false if no error) and returned data object 14 | */ 15 | PSNRequest.prototype.getProfile = function(username, callback) 16 | { 17 | this.Get( 18 | this.ReplacePSNUsername("https://{{region}}-prof.np.community.playstation.net/userProfile/v1/users/{{id}}/profile", username), 19 | { 20 | fields: profileFields 21 | }, 22 | callback 23 | ); 24 | }; 25 | 26 | /** Get current user's message groups 27 | * @param callback Callback function with error (false if no error) and returned data object 28 | */ 29 | PSNRequest.prototype.getMessageGroups = function(callback) 30 | { 31 | this.Get( 32 | "https://{{region}}-gmsg.np.community.playstation.net/groupMessaging/v1/users/{{psn}}/messageGroups", 33 | { 34 | fields: messageFields 35 | }, 36 | callback 37 | ); 38 | }; 39 | 40 | /** Get data from a specific message. All this data can be found in getMessageGroups 41 | * @param messageGroupId Group ID requested message belongs to 42 | * @param messageUid Message ID to fetch 43 | * @param messageKind Kind of message (as int) 44 | * @param callback Callback function with error (false if no error) and returned data object 45 | */ 46 | PSNRequest.prototype.getMessageContent = function(messageGroupId, messageUid, messageKind, callback) 47 | { 48 | var contentKey = false; 49 | 50 | // convert kind ID to contentKey string 51 | messageKind = parseInt(messageKind); 52 | if (messageKind == 1) contentKey = "message"; // text (no attachment) 53 | else if (messageKind == 3) contentKey = "image-data-0"; // photo/image 54 | else if (messageKind == 1011) contentKey = "voice-data-0"; // voice data 55 | else if (messageKind == 8) contentKey = "store-url-0"; // PSN store link 56 | 57 | if (!contentKey) 58 | { 59 | // check js/people/groupmessage.js in PSN app to find contentKey types (and their kind IDs) 60 | if (callback) callback("Error: Unknown PSN message kind: " + messageKind); 61 | return; 62 | } 63 | 64 | this.Get( 65 | "https://{{region}}-gmsg.np.community.playstation.net/groupMessaging/v1/messageGroups/{{messageGroupId}}/messages/{{messageUid}}". 66 | replace("{{messageGroupId}}", this.CleanPSNList(messageGroupId)). 67 | replace("{{messageUid}}", parseInt(messageUid)), 68 | { 69 | contentKey: contentKey 70 | }, 71 | callback 72 | ); 73 | }; 74 | 75 | // list valid filters for activity feeds 76 | PSNRequest.prototype.activityTypes = [ 77 | "PURCHASED", 78 | "RATED", 79 | "PLAYED_WITH", 80 | "VIDEO_UPLOAD", 81 | "SCREENSHOT_UPLOAD", 82 | "PLAYED_GAME", 83 | "LAUNCHED_GAME_FIRST_TIME", 84 | "WATCHED_VIDEO", 85 | "TROPHY", 86 | "BROADCASTING", 87 | "LIKED", 88 | "PROFILE_ABOUT_ME", 89 | "PROFILE_PIC", 90 | "FRIENDED", 91 | "CONTENT_SHARE", 92 | "STORE_PROMO", 93 | "IN_GAME_POST" 94 | ]; 95 | 96 | /** Get the signed-in user's activity feed 97 | * @param feed type of feed, either "feed" or "news" (optional, defaults to "news") 98 | * @param filters array of strings to filter by (optional, defaults to no filters) 99 | * Allowed: PURCHASED, RATED, VIDEO_UPLOAD, SCREENSHOT_UPLOAD, PLAYED_GAME, STORE_PROMO, WATCHED_VIDEO, TROPHY, BROADCASTING, LIKED, PROFILE_PIC, FRIENDED and CONTENT_SHARE 100 | * Use an empty array (or leave out argument) for all types 101 | * @param page Page of feed to load (default: 0) 102 | * @param callback Callback function with error (false if no error) and returned data object 103 | */ 104 | PSNRequest.prototype.getLatestActivity = function(feed, filters, page, callback) 105 | { 106 | // handle defaults for missing feed or filters arguments 107 | if (typeof feed == "function") 108 | { 109 | callback = feed; 110 | feed = "news"; 111 | filters = []; 112 | page = 0; 113 | } 114 | else if (typeof filters == "function") 115 | { 116 | callback = filters; 117 | filters = []; 118 | page = 0; 119 | } 120 | else if (typeof page == "function") 121 | { 122 | callback = page; 123 | page = 0; 124 | } 125 | 126 | // check filters are valid 127 | for(var i=0; i21)) 180 | { 181 | return false; 182 | } 183 | 184 | return username; 185 | } 186 | 187 | /** Replace {{id}} with the supplied username after cleaning up the username */ 188 | this.ReplacePSNUsername = function(input, username) 189 | { 190 | // cleanup username 191 | username = parent.CheckPSN(username); 192 | 193 | // return replaced {{id}} if username is valid 194 | if (username && input) return input.replace(/{{id}}/g, username); 195 | 196 | // failthrough, return original input 197 | return input; 198 | } 199 | 200 | /** Clean up a NPCommID of any weird characters */ 201 | this.CleanNPCommID = function(titleId) 202 | { 203 | return titleId.replace(/[^a-zA-Z0-9_]/, ""); 204 | } 205 | 206 | /** Generate headers for a PSN request */ 207 | function GetHeaders(additional_headers) 208 | { 209 | var ret_headers = {}; 210 | // clone default headers into our return object (parsed) 211 | for(var key in headers) 212 | { 213 | ret_headers[key] = ParseStaches(headers[key]); 214 | } 215 | 216 | // add accept-language header based on our language 217 | ret_headers['Accept-Language'] = auth_obj.npLanguage + "," + languages.join(','); 218 | 219 | // add access token (if we have one) 220 | if (auth_obj.access_token) 221 | { 222 | ret_headers['Authorization'] = 'Bearer ' + auth_obj.access_token; 223 | } 224 | 225 | // add additional headers (if supplied) (parsed) 226 | if (additional_headers) for(var key in additional_headers) 227 | { 228 | ret_headers[key] = ParseStaches(additional_headers[key]); 229 | } 230 | 231 | return ret_headers; 232 | } 233 | 234 | /** Get OAuth data */ 235 | function GetOAuthData(additional_fields) 236 | { 237 | var ret_fields = {}; 238 | // clone base oauth settings 239 | for(var key in oauth_settings) 240 | { 241 | ret_fields[key] = ParseStaches(oauth_settings[key]); 242 | } 243 | 244 | // add additional fields (if supplied) 245 | if (additional_fields) for(var key in additional_fields) 246 | { 247 | ret_fields[key] = ParseStaches(additional_fields[key]); 248 | } 249 | 250 | return ret_fields; 251 | } 252 | 253 | /** Make a PSN GET request */ 254 | function URLGET(url, fields, method, callback) 255 | { 256 | // method variable is optional 257 | if (typeof method == "function") 258 | { 259 | callback = method; 260 | method = "GET"; 261 | } 262 | 263 | _URLGET(url, fields, GetHeaders({"Access-Control-Request-Method": method}), method, function(err, body) { 264 | if (err) 265 | { 266 | // got error, bounce it up 267 | if (callback) callback(err); 268 | return; 269 | } 270 | else 271 | { 272 | var JSONBody; 273 | if (typeof body == "object") 274 | { 275 | // already a JSON object? 276 | JSONBody = body; 277 | } 278 | else 279 | { 280 | // try to parse JSON body 281 | if (!body || !/\S/.test(body)) 282 | { 283 | // string is empty, return empty object 284 | if (callback) callback(false, {}); 285 | return; 286 | } 287 | else 288 | { 289 | try 290 | { 291 | JSONBody = JSON.parse(body); 292 | } 293 | catch(parse_err) 294 | { 295 | // error parsing JSON result 296 | Log("Parse JSON error: " + parse_err + "\r\nURL:\r\n" + url + "\r\nBody:\r\n" + body); 297 | if (callback) callback("Parse JSON error: " + parse_err); 298 | return; 299 | } 300 | } 301 | } 302 | 303 | // success! return JSON object 304 | if (callback) callback(false, JSONBody); 305 | } 306 | }); 307 | } 308 | function _URLGET(url, fields, headers, method, callback) 309 | { 310 | // method variable is optional 311 | if (typeof method == "function") 312 | { 313 | callback = method; 314 | method = "GET"; 315 | } 316 | 317 | var settings = 318 | { 319 | url: url, 320 | headers: headers, 321 | method: method 322 | }; 323 | 324 | // put fields in correct place in HTTP header depending on method 325 | if (method == "GET") 326 | { 327 | if (fields) settings.url = settings.url + "?" + querystring.stringify(fields); 328 | } 329 | else if (method == "POST") 330 | { 331 | if (fields) settings.json = fields; 332 | } 333 | 334 | // Make a PSN GET request using request() lib 335 | request( 336 | settings, 337 | function(err, response, body) 338 | { 339 | if (err) 340 | { 341 | Log("Request error: " + err); 342 | if (callback) callback("Request error: " + err); 343 | return; 344 | } 345 | else if (response.statusCode == 401) 346 | { 347 | Log("Got error " + response.statusCode+", refreshing access token"); 348 | // token expired, unset access token 349 | auth_obj.access_token = false; 350 | GetAccessToken(function(error) { 351 | if (error) 352 | { 353 | if (callback) callback("Failed to refresh token: " + error); 354 | return; 355 | } 356 | 357 | _URLGET(url, fields, headers, method, callback); 358 | }); 359 | return; 360 | } 361 | else if (response.statusCode > 300) 362 | { 363 | if (response.body && response.body.error) 364 | { 365 | if (response.body.error.code) 366 | { 367 | // got full error object 368 | if (callback) callback("Server error: " + response.body.error.code + " :: " + response.body.error.message); 369 | } 370 | else 371 | { 372 | // we also got a nice error message! 373 | if (callback) callback("Server error: " + response.body.error + " :: " + response.body.errorDescription); 374 | } 375 | return; 376 | } 377 | // server successfully returned, but returned an error 378 | var JSONBody; 379 | try 380 | { 381 | JSONBody = JSON.parse(response.body); 382 | } 383 | catch(error_err) 384 | { 385 | } 386 | 387 | if (JSONBody && JSONBody.error && JSONBody.error.message) 388 | { 389 | if (callback) callback("Server error: " + response.statusCode + ": " + JSONBody.error.code + " - " + JSONBody.error.message); 390 | } 391 | else 392 | { 393 | if (callback) callback("Server error: " + response.statusCode); 394 | } 395 | return; 396 | } 397 | else 398 | { 399 | // success! return body 400 | if (callback) callback(false, body, response); 401 | } 402 | } 403 | ); 404 | } 405 | 406 | /** Our parser function for replacing {{lang}} etc. */ 407 | function ParseStaches(input) 408 | { 409 | if (typeof input != "string") return input; 410 | 411 | // replace {{lang}} 412 | var ret = input.replace(/{{lang}}/g, auth_obj.npLanguage); 413 | // replace {{access_token}} 414 | if (auth_obj.access_token) ret = ret.replace(/{{access_token}}/g, auth_obj.access_token); 415 | // replace {{region}} 416 | if (auth_obj.region) ret = ret.replace(/{{region}}/g, auth_obj.region); 417 | // replace {{psn}} with current user's PSN username 418 | if (auth_obj.username) ret = ret.replace(/{{psn}}/g, auth_obj.username); 419 | 420 | return ret; 421 | } 422 | 423 | /** Login to app and get auth token */ 424 | function Login(callback) 425 | { 426 | Log("Making OAuth Request..."); 427 | // start OAuth request (use our internal GET function as this is different!) 428 | _URLGET( 429 | // URL 430 | urls.login_base + "/2.0/oauth/authorize", 431 | // query string 432 | GetOAuthData({ 433 | response_type: "code", 434 | service_entity: "urn:service-entity:psn", 435 | returnAuthCode: true, 436 | state: "1156936032" 437 | }), 438 | // headers 439 | { 440 | 'User-Agent': ParseStaches(useragent) 441 | }, 442 | function(err, body, response) 443 | { 444 | if (err) 445 | { 446 | // return login error 447 | if (callback) callback(err); 448 | return; 449 | } 450 | else 451 | { 452 | // get the login path 453 | var login_referrer = response.req.path; 454 | 455 | Log("Logging in..."); 456 | 457 | // now actually login using the previous URL as the referrer 458 | request( 459 | { 460 | url: urls.login_base + "/login.do", 461 | method: "POST", 462 | headers: 463 | { 464 | 'User-Agent': ParseStaches(useragent), 465 | 'X-Requested-With': headers["X-Requested-With"], 466 | 'Origin': 'https://auth.api.sonyentertainmentnetwork.com', 467 | 'Referer': login_referrer, 468 | }, 469 | form: 470 | { 471 | 'params': new Buffer(login_params).toString('base64'), 472 | 'j_username': login_details.email, 473 | 'j_password': login_details.password 474 | } 475 | }, 476 | function (err, response, body) 477 | { 478 | if (err) 479 | { 480 | // login failed 481 | Log("Failed to make login request"); 482 | if (callback) callback(err); 483 | return; 484 | } 485 | else 486 | { 487 | Log("Following login..."); 488 | 489 | request.get(response.headers.location, function (err, response, body) 490 | { 491 | if (!err) 492 | { 493 | // parse URL 494 | var result = url.parse(response.req.path, true); 495 | if (result.query.authentication_error) 496 | { 497 | // try to extract login error from website 498 | var error_message = /errorDivMessage\"\>\s*(.*)\
/.exec(body); 499 | if (error_message && error_message[1]) 500 | { 501 | Log("Login failed! Error from Sony: " + error_message[1]); 502 | if (callback) callback(error_message[1]); 503 | } 504 | else 505 | { 506 | Log("Login failed!"); 507 | if (callback) callback("Login failed!"); 508 | } 509 | 510 | return; 511 | } 512 | else 513 | { 514 | // no auth error! 515 | var auth_result = url.parse(result.query.targetUrl, true); 516 | if (auth_result.query.authCode) 517 | { 518 | Log("Got auth code: " + auth_result.query.authCode); 519 | auth_obj.auth_code = auth_result.query.authCode; 520 | if (callback) callback(false); 521 | return; 522 | } 523 | else 524 | { 525 | Log("Auth error " + auth_result.query.error_code + ": " + auth_result.query.error_description); 526 | if (callback) callback("Auth error " + auth_result.query.error_code + ": " + auth_result.query.error_description); 527 | return; 528 | } 529 | } 530 | } 531 | else 532 | { 533 | Log("Auth code fetch error: " + err); 534 | if (callback) callback(err); 535 | return; 536 | } 537 | }); 538 | } 539 | } 540 | ); 541 | } 542 | } 543 | ); 544 | } 545 | 546 | /** Get an access token using the PSN oauth service using our current auth config */ 547 | function GetAccessToken(callback) 548 | { 549 | // do we have a refresh token? Or do we need to login from scratch? 550 | if (auth_obj.refresh_token) 551 | { 552 | // we have a refresh token! 553 | Log("Refreshing session..."); 554 | 555 | if (!auth_obj.refresh_token) 556 | { 557 | if (callback) callback("No refresh token found!"); 558 | return; 559 | } 560 | 561 | // request new access_token 562 | request.post( 563 | { 564 | url: urls.oauth, 565 | form: GetOAuthData({ 566 | "grant_type": "refresh_token", 567 | "refresh_token": auth_obj.refresh_token 568 | }) 569 | }, 570 | function(err, response, body) 571 | { 572 | _ParseTokenResponse(err, body, callback); 573 | } 574 | ); 575 | } 576 | else 577 | { 578 | // no refresh token, sign-in from scratch 579 | Log("Signing in with OAuth..."); 580 | 581 | // make sure we have an authcode 582 | if (!auth_obj.auth_code) 583 | { 584 | if (callback) callback("No authcode available for OAuth!"); 585 | return; 586 | } 587 | 588 | // request initial access_token 589 | request.post( 590 | { 591 | url: urls.oauth, 592 | form: GetOAuthData({ 593 | "grant_type": "authorization_code", 594 | "code": auth_obj.auth_code 595 | }) 596 | }, 597 | function(err, response, body) 598 | { 599 | _ParseTokenResponse(err, body, callback); 600 | } 601 | ); 602 | } 603 | } 604 | 605 | /** Helper function to parse OAuth responses (to save code duplication) */ 606 | function _ParseTokenResponse(err, body, callback) 607 | { 608 | if (err) 609 | { 610 | if (callback) callback("Request error: " + err); 611 | return; 612 | } 613 | 614 | // try to parse result 615 | var responseJSON; 616 | try 617 | { 618 | responseJSON = JSON.parse(body); 619 | } 620 | catch (JSONerror) 621 | { 622 | if (callback) callback("JSON Parse error: " + JSONerror); 623 | return; 624 | } 625 | 626 | // check server response for error 627 | if (responseJSON.error_description) 628 | { 629 | if (callback) callback("Server response error " + responseJSON.error_code + ": " + responseJSON.error_description); 630 | return; 631 | } 632 | 633 | // check we got an access token 634 | if (!responseJSON.access_token) 635 | { 636 | if (callback) callback("No access token received from PSN OAuth"); 637 | return; 638 | } 639 | 640 | Log("Got successful OAuth result"); 641 | 642 | // store our new tokens 643 | auth_obj.access_token = responseJSON.access_token; 644 | auth_obj.refresh_token = responseJSON.refresh_token; 645 | // calculate expire time of these tokens (shave off half a minute) 646 | auth_obj.expire_time = (new Date().getTime()) + ((responseJSON.expires_in - 30) * 1000); 647 | 648 | // save the data object 649 | DoSave(function() 650 | { 651 | // return no error 652 | if (callback) callback(false); 653 | } 654 | ); 655 | } 656 | 657 | /** Connect to PSN */ 658 | this.Connect = function(email, password, callback) 659 | { 660 | Log("Connecting to PSN..."); 661 | 662 | // check we have login details 663 | if (!email || !password) 664 | { 665 | Log("Missing email/password arguments to Connect()!"); 666 | 667 | if (callback) callback("Missing email/password arguments."); 668 | return; 669 | } 670 | 671 | // get auth token from login site 672 | Login(function(err, auth_code) { 673 | if (err) 674 | { 675 | if (callback) callback(err); 676 | } 677 | else 678 | { 679 | Log("Got authcode: " + auth_code); 680 | // store our auth_code 681 | auth_obj.auth_code = auth_code; 682 | 683 | GetAccessToken(function(error) { 684 | if (error) 685 | { 686 | Log(error); 687 | return; 688 | } 689 | 690 | // success! 691 | callback(false); 692 | }); 693 | } 694 | }); 695 | } 696 | 697 | /** Fetch the user's profile data */ 698 | function GetUserData(callback) 699 | { 700 | Log("Fetching user profile data"); 701 | 702 | // get the current user's data 703 | parent.Get("https://vl.api.np.km.playstation.net/vl/api/v1/mobile/users/me/info", {}, function(error, data) 704 | { 705 | if (error) 706 | { 707 | Log("Error fetching user profile: " + error); 708 | if (callback) callback("Error fetching user profile: " + error); 709 | return; 710 | } 711 | 712 | if (!data.onlineId) 713 | { 714 | Log("Missing PSNId from profile result: " + JSON.stringify(data, null, 2)); 715 | if (callback) callback("Missing PSNId from profile result: " + JSON.stringify(data, null, 2)); 716 | return; 717 | } 718 | 719 | // store user ID 720 | Log("We're logged in as " + data.onlineId); 721 | auth_obj.username = data.onlineId; 722 | // store user's region too 723 | auth_obj.region = data.region; 724 | 725 | // save updated data object 726 | DoSave(function() 727 | { 728 | // return no error 729 | if (callback) callback(false); 730 | } 731 | ); 732 | // supply self to let API know we are a token fetch function 733 | }, GetUserData); 734 | } 735 | 736 | /** Check the session's tokens are still valid! */ 737 | // token_fetch var is the function calling the token check in cases where the function is actually already fetching tokens! 738 | function CheckTokens(callback, token_fetch) 739 | { 740 | // build list of tokens we're missing 741 | var todo = []; 742 | 743 | // force token_fetch to an array 744 | token_fetch = [].concat(token_fetch); 745 | 746 | // make sure we're actually logged in first 747 | if (!auth_obj.auth_code) 748 | { 749 | Log("Need to login - no auth token found"); 750 | todo.push({name: "Login", func: Login}); 751 | } 752 | 753 | if (!auth_obj.expire_time || auth_obj.expire_time < new Date().getTime()) 754 | { 755 | // token has expired! Fetch access_token again 756 | Log("Need to fetch access tokens - tokens expired"); 757 | todo.push({name: "GetAccessToken", func: GetAccessToken}); 758 | } 759 | else if (!auth_obj.access_token) 760 | { 761 | // we have no access token (?!) 762 | Log("Need to fetch access tokens - no token available"); 763 | todo.push({name: "GetAccessToken", func: GetAccessToken}); 764 | } 765 | 766 | if (!auth_obj.username || !auth_obj.region) 767 | { 768 | // missing player username/region 769 | Log("Need to fetch userdata - no region or username available"); 770 | todo.push({name: "GetUserData", func: GetUserData}); 771 | } 772 | 773 | if (todo.length == 0) 774 | { 775 | // everything is fine 776 | if (callback) callback(false); 777 | } 778 | else 779 | { 780 | // work through our list of tokens we need to update 781 | var step = function() 782 | { 783 | var func = todo.shift(); 784 | if (!func) 785 | { 786 | // all done! 787 | if (callback) callback(false); 788 | return; 789 | } 790 | 791 | if (token_fetch.indexOf(func.func) >= 0) 792 | { 793 | // if we're actually calling a token fetch function, skip! 794 | process.nextTick(step); 795 | } 796 | else 797 | { 798 | func.func(function(error) { 799 | if (error) 800 | { 801 | // token fetching error! 802 | if (callback) callback(func.name + " :: " + error); 803 | return; 804 | } 805 | 806 | // do next step 807 | process.nextTick(step); 808 | }); 809 | } 810 | }; 811 | 812 | // start updating tokens 813 | process.nextTick(step); 814 | } 815 | } 816 | 817 | 818 | /** Make a PSN request */ 819 | function MakePSNRequest(method, url, fields, callback, token_fetch) 820 | { 821 | // use fields var as callback if it's missed out 822 | if (typeof fields == "function") 823 | { 824 | token_fetch = false; 825 | callback = fields; 826 | fields = {}; 827 | } 828 | else if (typeof method == "function") 829 | { 830 | token_fetch = false; 831 | callback = method; 832 | } 833 | 834 | // parse stache fields in fields list 835 | for(var field_key in fields) 836 | { 837 | fields[field_key] = ParseStaches(fields[field_key]); 838 | } 839 | 840 | CheckTokens(function(error) 841 | { 842 | // check our tokens are fine 843 | if (!error) 844 | { 845 | // make PSN GET request 846 | URLGET( 847 | // parse URL for region etc. 848 | ParseStaches(url), 849 | fields, 850 | method, 851 | function(error, data) 852 | { 853 | if (error) 854 | { 855 | Log("PSN " + method + " Error: " + error); 856 | if (callback) callback(error); 857 | return; 858 | } 859 | 860 | if (data.error && (data.error.code === 2105858 || data.error.code === 2138626)) 861 | { 862 | // access token has expired/failed/borked 863 | // login again! 864 | Log("Access token failure, try to login again."); 865 | Login(function(error) { 866 | if (error) 867 | { 868 | if (callback) callback(error); 869 | return; 870 | } 871 | 872 | // call ourselves 873 | parent.Get(url, fields, callback, token_fetch); 874 | }); 875 | } 876 | 877 | if (data.error && data.error.message) 878 | { 879 | // server error 880 | if (callback) callback(data.error.code + ": " + data.error.message, data.error); 881 | return; 882 | } 883 | 884 | // everything is fine! return data 885 | if (callback) callback(false, data); 886 | } 887 | ); 888 | } 889 | else 890 | { 891 | Log("Token error: " + error); 892 | if (callback) callback(error); 893 | return; 894 | } 895 | }, token_fetch); 896 | } 897 | 898 | 899 | this.Get = function(url, fields, callback, token_fetch) 900 | { 901 | MakePSNRequest("GET", url, fields, callback, token_fetch); 902 | } 903 | this.Post = function(url, fields, callback, token_fetch) 904 | { 905 | MakePSNRequest("POST", url, fields, callback, token_fetch); 906 | } 907 | this.Put = function(url, fields, callback, token_fetch) 908 | { 909 | MakePSNRequest("PUT", url, fields, callback, token_fetch); 910 | } 911 | this.Delete = function(url, fields, callback, token_fetch) 912 | { 913 | MakePSNRequest("DELETE", url, fields, callback, token_fetch); 914 | } 915 | 916 | 917 | /** Get the logged in user's PSN ID and region */ 918 | this.GetPSN = function(forceupdate, callback) 919 | { 920 | // forceupdate is optional 921 | if (typeof forceupdate == "function") 922 | { 923 | callback = forceupdate; 924 | forceupdate = false; 925 | } 926 | 927 | // call manual reload or just check tokens toggled on forceupdate bool 928 | var callFunc = forceupdate ? GetUserData : CheckTokens; 929 | callFunc(function(error) 930 | { 931 | if (error) 932 | { 933 | if (callback) callback(error); 934 | return; 935 | } 936 | 937 | // return username from auth data 938 | if (callback) callback(false, {psn: auth_obj.username, region: auth_obj.region}); 939 | } 940 | ); 941 | } 942 | 943 | /** Called when we're all setup */ 944 | function Ready() 945 | { 946 | if (options.autoconnect) 947 | { 948 | // make a connection request immediately (optional) 949 | parent.GetPSN(true, function() { 950 | if (options.onReady) options.onReady(); 951 | return; 952 | }); 953 | } 954 | else 955 | { 956 | // just callback that we're ready (if anyone is listening) 957 | if (options.onReady) options.onReady(); 958 | return; 959 | } 960 | } 961 | 962 | 963 | 964 | // load request library 965 | var request = require('request').defaults({ 966 | // use a cookie jar for logging in 967 | jar: true 968 | }); 969 | 970 | 971 | 972 | // init library 973 | if (options) 974 | { 975 | /** In-built optional debug log */ 976 | if (options.debug) 977 | { 978 | this.OnLog(DebugLog); 979 | } 980 | 981 | /** Optionally debug the Request lib */ 982 | if (options.requestDebug) 983 | { 984 | require('request').debug = true; 985 | } 986 | 987 | // store email and password 988 | if (options.email && options.password) 989 | { 990 | login_details.email = options.email; 991 | login_details.password = options.password; 992 | } 993 | 994 | // optionally read/write to an authfile 995 | if (options.authfile) 996 | { 997 | // register to OnSave 998 | parent.OnSave(function(data, callback) 999 | { 1000 | fs.writeFile(options.authfile, data, function(err) 1001 | { 1002 | if (err) 1003 | { 1004 | Log("Failed to write save data: " + err); 1005 | } 1006 | 1007 | // always call the callback anyway 1008 | if (callback) callback(); 1009 | }); 1010 | }); 1011 | 1012 | // load up file (if it already exists) 1013 | var fs = require("fs"); 1014 | fs.exists(options.authfile, function(exists) 1015 | { 1016 | if (exists) 1017 | { 1018 | // load previously saved data 1019 | fs.readFile(options.authfile, 'ascii', function(err, data) 1020 | { 1021 | parent.Load(data, function(err) 1022 | { 1023 | if (err) 1024 | { 1025 | console.log(err); 1026 | return; 1027 | } 1028 | 1029 | // mark as ready 1030 | Ready(); 1031 | }); 1032 | }); 1033 | } 1034 | else 1035 | { 1036 | // couldn't find file, but still mark ready to go! 1037 | Ready(); 1038 | } 1039 | }); 1040 | } 1041 | } 1042 | } 1043 | 1044 | // expose entire PSNObj class 1045 | module.exports = PSNObj; --------------------------------------------------------------------------------