├── .gitignore ├── package.json ├── LICENSE ├── README.md ├── scratchapi.d.ts └── scratchapi.js /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .scratchSession 3 | test.js 4 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "scratch-api", 3 | "version": "1.1.12", 4 | "description": "An API to interact with the Scratch 2.0 website", 5 | "main": "scratchapi.js", 6 | "typings": "./scratchapi.d.ts", 7 | "repository": { 8 | "type": "git", 9 | "url": "git@github.com:trumank/scratch-api.git" 10 | }, 11 | "author": "Truman Kilen", 12 | "license": "MIT", 13 | "bugs": { 14 | "url": "https://github.com/trumank/scratch-api/issues" 15 | }, 16 | "homepage": "https://github.com/trumank/scratch-api", 17 | "dependencies": { 18 | "prompt": "^1.1.0", 19 | "ws": "^7.0.1" 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 Truman Kilen 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 all 13 | 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 THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # scratch-api 2 | 3 | A utility for interacting with the Scratch 2.0 website. 4 | 5 | ## Installation 6 | 7 | Install with npm: 8 | 9 | ```sh 10 | npm install scratch-api 11 | ``` 12 | Or by cloning this repository: 13 | ```sh 14 | git clone https://github.com/trumank/scratch-api.git 15 | ``` 16 | 17 | ## Examples 18 | 19 | Sets the user's backpack to a single script. 20 | ```javascript 21 | var Scratch = require('scratch-api'); 22 | Scratch.UserSession.load(function(err, user) { 23 | if (err) return console.error(err); 24 | user.setBackpack([{ 25 | type: 'script', 26 | name: '', 27 | scripts: [[['say:', 'Cheers!']]] 28 | }], 29 | function(err, res) { 30 | if (err) return console.error(err); 31 | console.log('Backpack set'); 32 | }); 33 | }); 34 | ``` 35 | 36 | Prints all of the cloud variables for the given project. 37 | ```javascript 38 | var Scratch = require('scratch-api'); 39 | 40 | Scratch.UserSession.load(function(err, user) { 41 | user.cloudSession(, function(err, cloud) { 42 | cloud.on('set', function(name, value) { 43 | console.log(name, value); 44 | }); 45 | }); 46 | }); 47 | ``` 48 | 49 | ## See Also 50 | 51 | This scratch-api module is setup to be easily added too and extended. If you need to make certain requests that are not present it should be easy to add them. The [Scratch Wiki](http://wiki.scratch.mit.edu/wiki/Scratch_API_(2.0)) has some pretty extensive documentation. 52 | 53 | If you are feeling Pythonic today, check out Dylan Beswick's very similar [module for Python](https://github.com/Dylan5797/scratchapi). 54 | 55 | ## API 56 | 57 | ### Scratch 58 | * [`getProject`](#getProject) 59 | * [`getProjects`](#getProjects) 60 | 61 | ### Scratch.UserSession 62 | * [`static create`](#UserSession.create) 63 | * [`static prompt`](#UserSession.prompt) 64 | * [`static load`](#UserSession.load) 65 | * [`verify`](#UserSession.verify) 66 | * [`getProject`](#UserSession.getProject) 67 | * [`setProject`](#UserSession.setProject) 68 | * [`getProjects`](#UserSession.getProjects) 69 | * [`getAllProjects`](#UserSession.getAllProjects) 70 | * [`getBackpack`](#UserSession.getBackpack) 71 | * [`setBackpack`](#UserSession.setBackpack) 72 | * [`addComment`](#UserSession.addComment) 73 | * [`cloudSession`](#UserSession.cloudSession) 74 | 75 | ### Scratch.CloudSession 76 | * [`end`](#CloudSession.end) 77 | * [`get`](#CloudSession.get) 78 | * [`set`](#CloudSession.set) 79 | * [`variables`](#CloudSession.variables) 80 | * [`Event: set`](#CloudSession._set) 81 | * [`Event: end`](#CloudSession._end) 82 | 83 | ## Scratch 84 | 85 | 86 | ### static getProject(projectId, callback) 87 | 88 | Retrieves a JSON object of the given Scratch project. 89 | 90 | * `projectId` - The project's ID. 91 | * `callback(err, project)` 92 | 93 | 94 | ### static getProjects(username, callback) 95 | 96 | Retrieves a list of all public projects belonging to given user. 97 | 98 | * `username` - Username of owner 99 | * `callback(err, projects)` 100 | 101 | ## UserSession 102 | 103 | 104 | ### static create(username, password, callback) 105 | 106 | Creates a new Scratch session by signing in with the given username and password. 107 | 108 | * `username` - The Scratch account username (not case sensitive). 109 | * `password` - The Scratch account password. 110 | * `callback(err, user)` 111 | 112 | 113 | ### static prompt(callback) 114 | 115 | Creates a new Scratch session by prompting for the username and password via the command line. 116 | 117 | * `callback(err, user)` 118 | 119 | 120 | ### static load(callback) 121 | 122 | Attempts to create a user from a saved .scratchSession file. If one is not found, [`prompt`](#UserSession.prompt) is used instead and a .scratchSession file is created. 123 | 124 | * `callback(err, user)` 125 | 126 | 127 | ### verify(callback) 128 | 129 | Verifies that the user session is fresh and is ready to be used. 130 | 131 | * `callback(err, valid)` 132 | 133 | 134 | ### getProject(projectId, callback) - alias getProject 135 | 136 | 137 | ### setProject(projectId, payload, callback) 138 | 139 | Uploads the given payload object or string to the project with the given ID. The user must own the given project or the request will fail. 140 | 141 | * `projectId` - The project's ID. 142 | * `payload` - A JSON object or string. If it is an object, it will be stringified before sent. 143 | * `callback(err)` 144 | 145 | 146 | ### getProjects(callback) - alias getProjects 147 | 148 | 149 | ### getAllProject(callback) 150 | 151 | Gets a list of all projects (shared and unshared) belonging to the user. 152 | 153 | Note: This does not share the same format as `getProjects` as it uses the internal API. 154 | 155 | * `callback(err, projects)` 156 | 157 | 158 | ### getBackpack(callback) 159 | 160 | Retrieves the signed in user's backpack as a JSON object. 161 | 162 | * `callback(err, payload)` 163 | 164 | 165 | ### setBackpack(payload, callback) 166 | 167 | Uploads the given payload to the user's backpack. 168 | 169 | * `payload` - A JSON object or a string to be uploaded. 170 | * `callback(err)` 171 | 172 | 173 | ### addComment(options, callback) 174 | 175 | Comments on a project, profile, or studio. 176 | 177 | * `options` - A JSON object containing options. 178 | * `project`, `user`, or `studio`: The function checks (in that order) for these values. The user must be a username to post to, and all others must be ids. 179 | * `parent`: The comment id to reply to. Optional. 180 | * `replyto`: The user id to address (@username ...). Optional. 181 | * `content`: The text of the comment to post. 182 | * `callback(err)` 183 | 184 | 185 | ### cloudSession(projectId, callback) 186 | 187 | Connects to a cloud variable session for the given project. 188 | 189 | * `projectId` - The project's ID. 190 | * `callback(err, cloudSession)` 191 | 192 | ## Scratch.CloudSession 193 | 194 | 195 | ### end() 196 | 197 | Used to disconnect from the server and end the cloud session. 198 | 199 | 200 | ### get(name) 201 | 202 | Returns the value of a cloud variable or undefined if it does not exist. 203 | 204 | * `name` - The variable name including the cloud (☁) symbol. 205 | 206 | 207 | ### set(name, value) 208 | 209 | Sets the variable with the given name to the given value. 210 | 211 | * `name` - The variable name including the cloud (☁) symbol. 212 | * `value` - A new value. 213 | 214 | 215 | ### variables 216 | 217 | An object used as a hash table for all cloud variables. Variables can both read set directly via this object or through the corresponding [`get`](#CloudSession.get) and [`set`](#CloudSession.set) methods. 218 | 219 | 220 | ### Event: 'set' 221 | 222 | Emitted when a cloud variable is changed. 223 | 224 | * `name` - The variable name. 225 | * `value` - The variables new value. 226 | 227 | 228 | ### Event: 'end' 229 | 230 | Emitted when the server closes the connection (should never happen unless the client breaks). 231 | -------------------------------------------------------------------------------- /scratchapi.d.ts: -------------------------------------------------------------------------------- 1 | import EventEmitter from "events"; 2 | 3 | interface Project { 4 | id: number; 5 | title: string; 6 | description: string; 7 | instructions: string; 8 | visibility: string; 9 | public: boolean; 10 | comments_allowed: boolean; 11 | is_published: boolean; 12 | author: ProjectAuthor; 13 | image: string; 14 | images: ProjectImages; 15 | history: ProjectHistory; 16 | stats: ProjectStats; 17 | remix: ProjectRemix; 18 | } 19 | 20 | interface ProjectAuthor { 21 | id: number; 22 | username: string; 23 | scratchteam: boolean; 24 | history: AuthorHistory; 25 | profile: Profile; 26 | } 27 | 28 | interface AuthorHistory { 29 | joined: Date; 30 | } 31 | 32 | interface Profile { 33 | id: null; 34 | images: ProfileImages; 35 | } 36 | 37 | interface ProfileImages { 38 | "90x90": string; 39 | "60x60": string; 40 | "55x55": string; 41 | "50x50": string; 42 | "32x32": string; 43 | } 44 | 45 | interface ProjectHistory { 46 | created: string; 47 | modified: string; 48 | shared: string; 49 | } 50 | 51 | interface ProjectImages { 52 | "282x218": string; 53 | "216x163": string; 54 | "200x200": string; 55 | "144x108": string; 56 | "135x102": string; 57 | "100x80": string; 58 | } 59 | 60 | interface ProjectRemix { 61 | parent: null|number; 62 | root: null|number; 63 | } 64 | 65 | interface ProjectStats { 66 | views: number; 67 | loves: number; 68 | favorites: number; 69 | remixes: number; 70 | } 71 | 72 | type Commentable = "project"|"user"|"gallery"; 73 | 74 | interface CommentOptions { 75 | type: Commentable; 76 | content: string; 77 | parent: number; 78 | replyto: number; 79 | } 80 | 81 | /** 82 | * Get a project by its ID. 83 | * @param id The ID of the project to get. 84 | * @param cb The callback for when the project is found or not. 85 | */ 86 | export function getProject(id: number, cb: (err: Error|null, project: Project) => any); 87 | 88 | /** 89 | * Get all projects by a user. 90 | * @param id The ID of the user to get the projects for. 91 | * @param cb The callback for when the projects are found or not. 92 | */ 93 | export function getProjects(username: string, cb: (projects: Project[]) => any); 94 | 95 | export class UserSession { 96 | /** 97 | * The username of the current user. 98 | */ 99 | username: string; 100 | 101 | /** 102 | * The ID of the current user. 103 | */ 104 | id: number; 105 | 106 | /** 107 | * The current session identifier used in http requests. 108 | */ 109 | sessionId: string; 110 | 111 | /** 112 | * Create a new user session with the account's username and password. 113 | * @param username The username of the account to log into. 114 | * @param password The password of the account to log into. 115 | * @param cb The callback for when the session is created. 116 | * @example 117 | * ```typescript 118 | * const session = UserSession.create("weakeyes", "weakeyes123", function (err, session) { 119 | * if (err) 120 | * return console.error("An error occurred while connecting to scratch servers:", err); 121 | * 122 | * console.log("Logged in as", session.username, "(" + session.id + ")"); 123 | * }); 124 | * ``` 125 | */ 126 | static create(username: string, password: string, cb: (err: Error|null, session: UserSession) => any); 127 | 128 | /** 129 | * Prompt the terminal for account credentials. 130 | * @param cb The callback for when the session is created. 131 | * @example 132 | * ```typescript 133 | * const session = UserSession.prompt(function (err, session) { 134 | * if (err) 135 | * return console.error("An error occurred while prompting or connecting to scratch servers:", err); 136 | * 137 | * console.log("Logged in as", session.username, "(" + session.id + ")"); 138 | * }); 139 | * ``` 140 | */ 141 | static prompt(cb: (err: Error|null, session: UserSession) => any); 142 | 143 | /** 144 | * Load session information from a '.scratchSession' file in the current working directory. 145 | * @param cb The callback for when the session is created. 146 | * @example 147 | * ```typescript 148 | * UserSession.load(function (err, session) { 149 | * if (err) 150 | * return console.error("An error occurred while loading or connecting to scratch servers:", err); 151 | * 152 | * console.log("Logged in as", session.username, "(" + session.id + ")"); 153 | * }); 154 | * ``` 155 | */ 156 | static load(cb: (err: Error|null, session: UserSession) => any); 157 | 158 | /** 159 | * Instantiate a new user session given the sessionId. If you need to login with a username and password, see {@link UserSession.create}. 160 | * @param username The username of the account. 161 | * @param id The ID of the account profile. 162 | * @param sessionId The session ID to use. 163 | */ 164 | constructor(username: string, id: number, sessionId: string); 165 | 166 | /** 167 | * Save the current user session to a '.scratchSession' file in the current working directory. 168 | * @param cb The callback for when the session is saved. 169 | */ 170 | private _saveSession(cb: (err: Error|null) => any); 171 | 172 | /** 173 | * Verify if the user session is still authorized on scratch servers. 174 | * @param cb The callback for when the session is created. 175 | */ 176 | verify(cb: (err: Error|null, valid: boolean) => void); 177 | 178 | /** 179 | * Get a project by its ID. 180 | * @param id The ID of the project to get. 181 | * @param cb The callback for when the project is found or not. 182 | */ 183 | getProject(id: number, cb: (err: Error|null, project: Project) => any); 184 | 185 | /** 186 | * Get your own user's projects. Will only return some of them, use {@link UserSession.getAllProjects} to get every one. 187 | * @param cb The callback for when the projects are found or not. 188 | */ 189 | getProjects(cb: (err: Error|null, project: Project) => any); 190 | 191 | /** 192 | * Get all of your user's projects. 193 | * @param cb The callback for when the projects are found or not. 194 | */ 195 | getAllProjects(cb: (err: Error|null, projects: Project[]) => any); 196 | 197 | /** 198 | * Update a project by its ID. Warning: Only use this method if you know the JSON structure of projects. 199 | * @param projectId The ID of the project to update. 200 | * @param payload The updated project data. 201 | * @param cb The callback for when the project is updated or not. 202 | */ 203 | setProject(projectId: number, payload: any, cb: (err: Error|null) => any); 204 | 205 | /** 206 | * Retrieve the contents of your user's backpack. 207 | * @param cb The callback for when the backpack is retrieved or not. 208 | */ 209 | getBackpack(cb: (err: Error|null, backpack: any) => any); 210 | 211 | /** 212 | * Update your user's backpack. Warning: Only use this method if you know the JSON structure of user backpacks. 213 | * @param cb The callback for when the backpack is updated or not. 214 | */ 215 | setBackpack(payload: any, cb: (err: Error|null) => any); 216 | 217 | /** 218 | * Add a comment to a project, user or gallery. 219 | * @param options The options for the comment. 220 | * @param cb The callback for when the comment is added. 221 | */ 222 | addComment(options: CommentOptions, cb: (err: Error|null, comment: any) => any); 223 | 224 | /** 225 | * Create a new cloud data websocket session for a project. 226 | * @param projectId The ID of the project to listen to cloud data updates for. 227 | * @param cb The callback for when the cloud session is created. 228 | */ 229 | cloudSession(projectId: number, cb: (err: Error|null, cloudSession: CloudSession) => any); 230 | } 231 | 232 | export interface CloudSession extends EventEmitter { 233 | on(event: "set", listener: (name: string, value: string) => any): any; 234 | off(event: "set", listener: (name: string, value: string) => any): any; 235 | emit(event: "set", name: string, value: string): any; 236 | } 237 | 238 | export class CloudSession extends EventEmitter { 239 | user: UserSession; 240 | projectId: string; 241 | connection: null; 242 | attemptedPackets: []; 243 | variables: Record; 244 | 245 | private _variables: Record; 246 | 247 | constructor(user: UserSession, projectId: number); 248 | 249 | /** 250 | * Connect to the cloud session. 251 | * @param cb The callback for when the cloud session is connected. 252 | */ 253 | private _connect(cb: (err: Error|null) => any); 254 | 255 | /** 256 | * Handle a packet from the scratch servers. 257 | * @param packet The packet to be handled. 258 | */ 259 | private _handlePacket(packet: any); 260 | 261 | /** 262 | * Send the initial handshake when the connection is opened. 263 | */ 264 | private _sendHandshake(); 265 | 266 | /** 267 | * Send a packet updating the value of a variable. 268 | * @param name The name of the variable to update. 269 | * @param value The new value of the variable. 270 | */ 271 | private _sendSet(name: string, value: string); 272 | 273 | /** 274 | * Send a formatted packet. 275 | * @param method The method to use in the packet. 276 | * @param options Additional options for the packet. 277 | */ 278 | private _send(method: string, options: any); 279 | 280 | /** 281 | * Send a packet to the scratch servers. 282 | * @param data The data to send. 283 | */ 284 | private _sendPacket(data: string); 285 | 286 | /** 287 | * Add a variable internally. 288 | * @param name The name of the variable. 289 | * @param value The value of the variable. 290 | */ 291 | private _addVariable(name: string, value: string); 292 | 293 | /** 294 | * Create a new cloud session for a user and project. 295 | * @param user The user to authorize as. 296 | * @param projectId The ID of the project to connect to. 297 | * @param cb The callback for when the cloud session is created. 298 | */ 299 | _create(user: UserSession, projectId: number, cb: (err: Error, session: CloudSession) => any); 300 | 301 | /** 302 | * End the websocket connection. 303 | */ 304 | end(); 305 | 306 | /** 307 | * Get the value of a variable (Requires ☁ before the variable name). 308 | */ 309 | get(name: string): string; 310 | 311 | /** 312 | * Set the value of a variable (Requires ☁ before the variable name). 313 | */ 314 | set(name: string, value: string); 315 | } -------------------------------------------------------------------------------- /scratchapi.js: -------------------------------------------------------------------------------- 1 | var https = require('https'); 2 | var util = require('util'); 3 | var events = require('events'); 4 | var fs = require('fs'); 5 | var WebSocket = require('ws'); 6 | 7 | var SERVER = 'scratch.mit.edu'; 8 | var PROJECTS_SERVER = 'projects.scratch.mit.edu'; 9 | var CDN_SERVER = 'cdn.scratch.mit.edu'; 10 | var CLOUD_SERVER = 'clouddata.scratch.mit.edu'; 11 | var API_SERVER = 'api.scratch.mit.edu'; 12 | 13 | var SESSION_FILE = '.scratchSession'; 14 | 15 | function request(options, cb) { 16 | var headers = { 17 | 'Cookie': 'scratchcsrftoken=a; scratchlanguage=en;', 18 | 'X-CSRFToken': 'a', 19 | 'referer': 'https://scratch.mit.edu' // Required by Scratch servers 20 | }; 21 | if (options.headers) { 22 | for (var name in options.headers) { 23 | headers[name] = options.headers[name]; 24 | } 25 | } 26 | if (options.body) headers['Content-Length'] = Buffer.byteLength(options.body); 27 | if (options.sessionId) headers.Cookie += 'scratchsessionsid=' + options.sessionId + ';'; 28 | var req = https.request({ 29 | hostname: options.hostname || SERVER, 30 | port: 443, 31 | path: options.path, 32 | method: options.method || 'GET', 33 | headers: headers 34 | }, function(response) { 35 | var parts = []; 36 | response.on('data', function(chunk) { parts.push(chunk); }); 37 | response.on('end', function() { cb(null, Buffer.concat(parts).toString(), response); }); 38 | }); 39 | req.on('error', cb); 40 | if (options.body) req.write(options.body); 41 | req.end(); 42 | } 43 | 44 | function requestJSON(options, cb) { 45 | request(options, function(err, body, response) { 46 | if (err) return cb(err); 47 | try { 48 | cb(null, JSON.parse(body)); 49 | } catch (e) { 50 | cb(e); 51 | } 52 | }); 53 | } 54 | 55 | function parseCookie(cookie) { 56 | var cookies = {}; 57 | var each = cookie.split(';'); 58 | var i = each.length; 59 | while (i--) { 60 | if (each[i].indexOf('=') === -1) { 61 | continue; 62 | } 63 | var pair = each[i].split('='); 64 | cookies[pair[0].trim()] = pair[1].trim(); 65 | } 66 | return cookies; 67 | } 68 | 69 | var Scratch = {}; 70 | 71 | Scratch.getProject = function(projectId, cb) { 72 | requestJSON({ 73 | hostname: PROJECTS_SERVER, 74 | path: '/' + projectId, 75 | method: 'GET', 76 | }, cb); 77 | }; 78 | Scratch.getProjects = function(username, cb) { 79 | requestJSON({ 80 | hostname: API_SERVER, 81 | path: '/users/' + username + '/projects', 82 | method: 'GET' 83 | }, cb); 84 | }; 85 | Scratch.UserSession = function(username, id, sessionId) { 86 | this.username = username; 87 | this.id = id; 88 | this.sessionId = sessionId; 89 | }; 90 | Scratch.UserSession.create = function(username, password, cb) { 91 | request({ 92 | path: '/login/', 93 | method: 'POST', 94 | body: JSON.stringify({username: username, password: password}), 95 | headers: {'X-Requested-With': 'XMLHttpRequest'} 96 | }, function(err, body, response) { 97 | if (err) return cb(err); 98 | try { 99 | var user = JSON.parse(body)[0]; 100 | if (user.msg) return cb(new Error(user.msg)); 101 | cb(null, new Scratch.UserSession(user.username, user.id, parseCookie(response.headers['set-cookie'][0]).scratchsessionsid)); 102 | } catch (e) { 103 | cb(e); 104 | } 105 | }); 106 | }; 107 | Scratch.UserSession.prompt = function(cb) { 108 | var prompt = require('prompt'); 109 | prompt.start(); 110 | prompt.get([ 111 | { name: 'username' }, 112 | { name: 'password', hidden: true } 113 | ], function(err, results) { 114 | if (err) return cb(err); 115 | Scratch.UserSession.create(results.username, results.password, cb); 116 | }); 117 | }; 118 | Scratch.UserSession.load = function(cb) { 119 | function prompt() { 120 | Scratch.UserSession.prompt(function(err, session) { 121 | if (err) return cb(err); 122 | session._saveSession(function() { 123 | cb(null, session); 124 | }); 125 | }); 126 | } 127 | fs.readFile(SESSION_FILE, function(err, data) { 128 | if (err) return prompt(); 129 | var obj = JSON.parse(data.toString()); 130 | var session = new Scratch.UserSession(obj.username, obj.id, obj.sessionId); 131 | session.verify(function(err, valid) { 132 | if (err) return cb(err); 133 | if (valid) return cb(null, session); 134 | prompt(); 135 | }); 136 | }); 137 | }; 138 | Scratch.UserSession.prototype._saveSession = function(cb) { 139 | fs.writeFile(SESSION_FILE, JSON.stringify({ 140 | username: this.username, 141 | id: this.id, 142 | sessionId: this.sessionId 143 | }), cb); 144 | }; 145 | Scratch.UserSession.prototype.verify = function(cb) { 146 | request({ 147 | path: '/messages/ajax/get-message-count/', // probably going to change quite soon 148 | sessionId: this.sessionId 149 | }, function(err, body, response) { 150 | cb(null, !err && response.statusCode === 200); 151 | }); 152 | }; 153 | Scratch.UserSession.prototype.getProject = Scratch.getProject; 154 | Scratch.UserSession.prototype.getProjects = function(cb) { 155 | Scratch.getProjects(this.username, cb); 156 | }; 157 | 158 | Scratch.UserSession.prototype.getAllProjects = function(cb) { 159 | requestJSON({ 160 | hostname: SERVER, 161 | path: '/site-api/projects/all/', 162 | method: 'GET', 163 | sessionId: this.sessionId 164 | }, cb); 165 | }; 166 | Scratch.UserSession.prototype.setProject = function(projectId, payload, cb) { 167 | if (typeof payload !== 'string') payload = JSON.stringify(payload); 168 | requestJSON({ 169 | hostname: PROJECTS_SERVER, 170 | path: '/internalapi/project/' + projectId + '/set/', 171 | method: 'POST', 172 | body: payload, 173 | sessionId: this.sessionId 174 | }, cb); 175 | }; 176 | Scratch.UserSession.prototype.getBackpack = function(cb) { 177 | requestJSON({ 178 | hostname: SERVER, 179 | path: '/internalapi/backpack/' + this.username + '/get/', 180 | method: 'GET', 181 | sessionId: this.sessionId 182 | }, cb); 183 | }; 184 | Scratch.UserSession.prototype.setBackpack = function(payload, cb) { 185 | if (typeof payload !== 'string') payload = JSON.stringify(payload); 186 | requestJSON({ 187 | hostname: SERVER, 188 | path: '/internalapi/backpack/' + this.username + '/set/', 189 | method: 'POST', 190 | body: payload, 191 | sessionId: this.sessionId 192 | }, cb); 193 | }; 194 | Scratch.UserSession.prototype.addComment = function(options, cb) { 195 | var type, id; 196 | if (options.project) { 197 | type = 'project'; 198 | id = options.project; 199 | } else if (options.user) { 200 | type = 'user'; 201 | id = options.user; 202 | } else if (options.studio) { 203 | type = 'gallery'; 204 | id = options.studio; 205 | } 206 | request({ 207 | hostname: SERVER, 208 | path: '/site-api/comments/' + type + '/' + id + '/add/', 209 | method: 'POST', 210 | body: JSON.stringify({ 211 | content: options.content, 212 | parent_id: options.parent || '', 213 | commentee_id: options.replyto || '', 214 | }), 215 | sessionId: this.sessionId 216 | }, cb); 217 | }; 218 | Scratch.UserSession.prototype.cloudSession = function(projectId, cb) { 219 | Scratch.CloudSession._create(this, projectId, cb); 220 | }; 221 | 222 | Scratch.CloudSession = function(user, projectId) { 223 | this.user = user; 224 | this.projectId = '' + projectId; 225 | this.connection = null; 226 | this.attemptedPackets = []; 227 | this.variables = Object.create(null); 228 | this._variables = Object.create(null); 229 | }; 230 | util.inherits(Scratch.CloudSession, events.EventEmitter); 231 | Scratch.CloudSession._create = function(user, projectId, cb) { 232 | var session = new Scratch.CloudSession(user, projectId); 233 | session._connect(function(err) { 234 | if (err) return cb(err); 235 | cb(null, session); 236 | }); 237 | }; 238 | Scratch.CloudSession.prototype._connect = function(cb) { 239 | var self = this; 240 | 241 | this.connection = new WebSocket('wss://' + CLOUD_SERVER + '/', [], { 242 | headers: { 243 | cookie: 'scratchsessionsid=' + this.user.sessionId + ';', 244 | origin: 'https://scratch.mit.edu' 245 | } 246 | } 247 | ); 248 | this.connection.on('open', function() { 249 | self._sendHandshake(); 250 | for (var i = 0; i < self.attemptedPackets.length; i++) { 251 | self._sendPacket(self.attemptedPackets[i]); 252 | } 253 | self.attemptedPackets = []; 254 | if (cb) cb(); 255 | }); 256 | 257 | this.connection.on('close', function() { 258 | // Reconnect because Scratch disconnects clients after no activity 259 | // Probably will cause some data to not be pushed 260 | self._connect(); 261 | }); 262 | 263 | var stream = ''; 264 | this.connection.on('message', function(chunk) { 265 | stream += chunk; 266 | var packets = stream.split('\n'); 267 | for(var i = 0; i < packets.length - 1; i++) { 268 | var line = packets[i]; 269 | var packet; 270 | try { 271 | packet = JSON.parse(line); 272 | } catch (err) { 273 | console.warn('Invalid packet %s', line); 274 | return; 275 | } 276 | self._handlePacket(packet); 277 | } 278 | stream = packets[packets.length - 1]; 279 | }); 280 | }; 281 | Scratch.CloudSession.prototype.end = function() { 282 | if (this.connection) { 283 | this.connection.close(); 284 | } 285 | }; 286 | Scratch.CloudSession.prototype.get = function(name) { 287 | return this._variables[name]; 288 | }; 289 | Scratch.CloudSession.prototype.set = function(name, value) { 290 | this._variables[name] = value; 291 | this._sendSet(name, value); 292 | }; 293 | Scratch.CloudSession.prototype._handlePacket = function(packet) { 294 | switch (packet.method) { 295 | case 'set': 296 | if (!({}).hasOwnProperty.call(this.variables, packet.name)) { 297 | this._addVariable(packet.name, packet.value); 298 | } 299 | this._variables[packet.name] = packet.value; 300 | this.emit('set', packet.name, packet.value); 301 | break; 302 | default: 303 | console.warn('Unimplemented packet', packet.method); 304 | } 305 | }; 306 | Scratch.CloudSession.prototype._sendHandshake = function() { 307 | this._send('handshake', {}); 308 | }; 309 | Scratch.CloudSession.prototype._sendSet = function(name, value) { 310 | this._send('set', { 311 | name: name, 312 | value: value 313 | }); 314 | }; 315 | Scratch.CloudSession.prototype._send = function(method, options) { 316 | var object = { 317 | user: this.user.username, 318 | project_id: this.projectId, 319 | method: method 320 | }; 321 | for (var name in options) { 322 | object[name] = options[name]; 323 | } 324 | 325 | this._sendPacket(JSON.stringify(object) + '\n'); 326 | }; 327 | Scratch.CloudSession.prototype._sendPacket = function(data) { 328 | if (this.connection.readyState === WebSocket.OPEN) { 329 | this.connection.send(data); 330 | } else { 331 | this.attemptedPackets.push(data); 332 | } 333 | }; 334 | Scratch.CloudSession.prototype._addVariable = function(name, value) { 335 | var self = this; 336 | this._variables[name] = value; 337 | Object.defineProperty(this.variables, name, { 338 | enumerable: true, 339 | get: function() { 340 | return self.get(name); 341 | }, 342 | set: function(value) { 343 | self.set(name, value); 344 | } 345 | }); 346 | }; 347 | 348 | module.exports = Scratch; 349 | --------------------------------------------------------------------------------