├── .gitignore ├── LICENSE ├── README.md ├── package-lock.json ├── package.json ├── scratchapi.d.ts └── scratchapi.js /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .scratchSession 3 | test.js 4 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /package-lock.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "scratch-api", 3 | "version": "1.1.12", 4 | "lockfileVersion": 2, 5 | "requires": true, 6 | "packages": { 7 | "": { 8 | "version": "1.1.12", 9 | "license": "MIT", 10 | "dependencies": { 11 | "prompt": "^1.1.0", 12 | "ws": "^7.0.1" 13 | } 14 | }, 15 | "node_modules/async": { 16 | "version": "0.9.2", 17 | "resolved": "https://registry.npmjs.org/async/-/async-0.9.2.tgz", 18 | "integrity": "sha1-rqdNXmHB+JlhO/ZL2mbUx48v0X0=" 19 | }, 20 | "node_modules/balanced-match": { 21 | "version": "1.0.2", 22 | "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", 23 | "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==" 24 | }, 25 | "node_modules/brace-expansion": { 26 | "version": "1.1.11", 27 | "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", 28 | "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", 29 | "dependencies": { 30 | "balanced-match": "^1.0.0", 31 | "concat-map": "0.0.1" 32 | } 33 | }, 34 | "node_modules/colors": { 35 | "version": "1.4.0", 36 | "resolved": "https://registry.npmjs.org/colors/-/colors-1.4.0.tgz", 37 | "integrity": "sha512-a+UqTh4kgZg/SlGvfbzDHpgRu7AAQOmmqRHJnxhRZICKFUT91brVhNNt58CMWU9PsBbv3PDCZUHbVxuDiH2mtA==", 38 | "engines": { 39 | "node": ">=0.1.90" 40 | } 41 | }, 42 | "node_modules/concat-map": { 43 | "version": "0.0.1", 44 | "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", 45 | "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=" 46 | }, 47 | "node_modules/cycle": { 48 | "version": "1.0.3", 49 | "resolved": "https://registry.npmjs.org/cycle/-/cycle-1.0.3.tgz", 50 | "integrity": "sha1-IegLK+hYD5i0aPN5QwZisEbDStI=", 51 | "engines": { 52 | "node": ">=0.4.0" 53 | } 54 | }, 55 | "node_modules/deep-equal": { 56 | "version": "0.2.2", 57 | "resolved": "https://registry.npmjs.org/deep-equal/-/deep-equal-0.2.2.tgz", 58 | "integrity": "sha1-hLdFiW80xoTpjyzg5Cq69Du6AX0=" 59 | }, 60 | "node_modules/eyes": { 61 | "version": "0.1.8", 62 | "resolved": "https://registry.npmjs.org/eyes/-/eyes-0.1.8.tgz", 63 | "integrity": "sha1-Ys8SAjTGg3hdkCNIqADvPgzCC8A=", 64 | "engines": { 65 | "node": "> 0.1.90" 66 | } 67 | }, 68 | "node_modules/fs.realpath": { 69 | "version": "1.0.0", 70 | "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", 71 | "integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8=" 72 | }, 73 | "node_modules/glob": { 74 | "version": "7.1.7", 75 | "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.7.tgz", 76 | "integrity": "sha512-OvD9ENzPLbegENnYP5UUfJIirTg4+XwMWGaQfQTY0JenxNvvIKP3U3/tAQSPIu/lHxXYSZmpXlUHeqAIdKzBLQ==", 77 | "dependencies": { 78 | "fs.realpath": "^1.0.0", 79 | "inflight": "^1.0.4", 80 | "inherits": "2", 81 | "minimatch": "^3.0.4", 82 | "once": "^1.3.0", 83 | "path-is-absolute": "^1.0.0" 84 | }, 85 | "engines": { 86 | "node": "*" 87 | }, 88 | "funding": { 89 | "url": "https://github.com/sponsors/isaacs" 90 | } 91 | }, 92 | "node_modules/i": { 93 | "version": "0.3.6", 94 | "resolved": "https://registry.npmjs.org/i/-/i-0.3.6.tgz", 95 | "integrity": "sha1-2WyScyB28HJxG2sQ/X1PZa2O4j0=", 96 | "engines": { 97 | "node": ">=0.4" 98 | } 99 | }, 100 | "node_modules/inflight": { 101 | "version": "1.0.6", 102 | "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", 103 | "integrity": "sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=", 104 | "dependencies": { 105 | "once": "^1.3.0", 106 | "wrappy": "1" 107 | } 108 | }, 109 | "node_modules/inherits": { 110 | "version": "2.0.4", 111 | "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", 112 | "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==" 113 | }, 114 | "node_modules/isstream": { 115 | "version": "0.1.2", 116 | "resolved": "https://registry.npmjs.org/isstream/-/isstream-0.1.2.tgz", 117 | "integrity": "sha1-R+Y/evVa+m+S4VAOaQ64uFKcCZo=" 118 | }, 119 | "node_modules/minimatch": { 120 | "version": "3.0.4", 121 | "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz", 122 | "integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==", 123 | "dependencies": { 124 | "brace-expansion": "^1.1.7" 125 | }, 126 | "engines": { 127 | "node": "*" 128 | } 129 | }, 130 | "node_modules/minimist": { 131 | "version": "1.2.5", 132 | "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.5.tgz", 133 | "integrity": "sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw==" 134 | }, 135 | "node_modules/mkdirp": { 136 | "version": "0.5.5", 137 | "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.5.tgz", 138 | "integrity": "sha512-NKmAlESf6jMGym1++R0Ra7wvhV+wFW63FaSOFPwRahvea0gMUcGUhVeAg/0BC0wiv9ih5NYPB1Wn1UEI1/L+xQ==", 139 | "dependencies": { 140 | "minimist": "^1.2.5" 141 | }, 142 | "bin": { 143 | "mkdirp": "bin/cmd.js" 144 | } 145 | }, 146 | "node_modules/mute-stream": { 147 | "version": "0.0.8", 148 | "resolved": "https://registry.npmjs.org/mute-stream/-/mute-stream-0.0.8.tgz", 149 | "integrity": "sha512-nnbWWOkoWyUsTjKrhgD0dcz22mdkSnpYqbEjIm2nhwhuxlSkpywJmBo8h0ZqJdkp73mb90SssHkN4rsRaBAfAA==" 150 | }, 151 | "node_modules/ncp": { 152 | "version": "1.0.1", 153 | "resolved": "https://registry.npmjs.org/ncp/-/ncp-1.0.1.tgz", 154 | "integrity": "sha1-0VNn5cuHQyuhF9K/gP30Wuz7QkY=", 155 | "bin": { 156 | "ncp": "bin/ncp" 157 | } 158 | }, 159 | "node_modules/once": { 160 | "version": "1.4.0", 161 | "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", 162 | "integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=", 163 | "dependencies": { 164 | "wrappy": "1" 165 | } 166 | }, 167 | "node_modules/path-is-absolute": { 168 | "version": "1.0.1", 169 | "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", 170 | "integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18=", 171 | "engines": { 172 | "node": ">=0.10.0" 173 | } 174 | }, 175 | "node_modules/prompt": { 176 | "version": "1.1.0", 177 | "resolved": "https://registry.npmjs.org/prompt/-/prompt-1.1.0.tgz", 178 | "integrity": "sha512-ec1vUPXCplDBDUVD8uPa3XGA+OzLrO40Vxv3F1uxoiZGkZhdctlK2JotcHq5X6ExjocDOGwGdCSXloGNyU5L1Q==", 179 | "dependencies": { 180 | "colors": "^1.1.2", 181 | "read": "1.0.x", 182 | "revalidator": "0.1.x", 183 | "utile": "0.3.x", 184 | "winston": "2.x" 185 | }, 186 | "engines": { 187 | "node": ">= 0.6.6" 188 | } 189 | }, 190 | "node_modules/read": { 191 | "version": "1.0.7", 192 | "resolved": "https://registry.npmjs.org/read/-/read-1.0.7.tgz", 193 | "integrity": "sha1-s9oZvQUkMal2cdRKQmNK33ELQMQ=", 194 | "dependencies": { 195 | "mute-stream": "~0.0.4" 196 | }, 197 | "engines": { 198 | "node": ">=0.8" 199 | } 200 | }, 201 | "node_modules/revalidator": { 202 | "version": "0.1.8", 203 | "resolved": "https://registry.npmjs.org/revalidator/-/revalidator-0.1.8.tgz", 204 | "integrity": "sha1-/s5hv6DBtSoga9axgZgYS91SOjs=", 205 | "engines": { 206 | "node": ">= 0.4.0" 207 | } 208 | }, 209 | "node_modules/rimraf": { 210 | "version": "2.7.1", 211 | "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.7.1.tgz", 212 | "integrity": "sha512-uWjbaKIK3T1OSVptzX7Nl6PvQ3qAGtKEtVRjRuazjfL3Bx5eI409VZSqgND+4UNnmzLVdPj9FqFJNPqBZFve4w==", 213 | "dependencies": { 214 | "glob": "^7.1.3" 215 | }, 216 | "bin": { 217 | "rimraf": "bin.js" 218 | } 219 | }, 220 | "node_modules/stack-trace": { 221 | "version": "0.0.10", 222 | "resolved": "https://registry.npmjs.org/stack-trace/-/stack-trace-0.0.10.tgz", 223 | "integrity": "sha1-VHxws0fo0ytOEI6hoqFZ5f3eGcA=", 224 | "engines": { 225 | "node": "*" 226 | } 227 | }, 228 | "node_modules/utile": { 229 | "version": "0.3.0", 230 | "resolved": "https://registry.npmjs.org/utile/-/utile-0.3.0.tgz", 231 | "integrity": "sha1-E1LDQOuCDk2N26A5pPv6oy7U7zo=", 232 | "dependencies": { 233 | "async": "~0.9.0", 234 | "deep-equal": "~0.2.1", 235 | "i": "0.3.x", 236 | "mkdirp": "0.x.x", 237 | "ncp": "1.0.x", 238 | "rimraf": "2.x.x" 239 | }, 240 | "engines": { 241 | "node": ">= 0.8.0" 242 | } 243 | }, 244 | "node_modules/winston": { 245 | "version": "2.4.5", 246 | "resolved": "https://registry.npmjs.org/winston/-/winston-2.4.5.tgz", 247 | "integrity": "sha512-TWoamHt5yYvsMarGlGEQE59SbJHqGsZV8/lwC+iCcGeAe0vUaOh+Lv6SYM17ouzC/a/LB1/hz/7sxFBtlu1l4A==", 248 | "dependencies": { 249 | "async": "~1.0.0", 250 | "colors": "1.0.x", 251 | "cycle": "1.0.x", 252 | "eyes": "0.1.x", 253 | "isstream": "0.1.x", 254 | "stack-trace": "0.0.x" 255 | }, 256 | "engines": { 257 | "node": ">= 0.10.0" 258 | } 259 | }, 260 | "node_modules/winston/node_modules/async": { 261 | "version": "1.0.0", 262 | "resolved": "https://registry.npmjs.org/async/-/async-1.0.0.tgz", 263 | "integrity": "sha1-+PwEyjoTeErenhZBr5hXjPvWR6k=" 264 | }, 265 | "node_modules/winston/node_modules/colors": { 266 | "version": "1.0.3", 267 | "resolved": "https://registry.npmjs.org/colors/-/colors-1.0.3.tgz", 268 | "integrity": "sha1-BDP0TYCWgP3rYO0mDxsMJi6CpAs=", 269 | "engines": { 270 | "node": ">=0.1.90" 271 | } 272 | }, 273 | "node_modules/wrappy": { 274 | "version": "1.0.2", 275 | "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", 276 | "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=" 277 | }, 278 | "node_modules/ws": { 279 | "version": "7.4.6", 280 | "resolved": "https://registry.npmjs.org/ws/-/ws-7.4.6.tgz", 281 | "integrity": "sha512-YmhHDO4MzaDLB+M9ym/mDA5z0naX8j7SIlT8f8z+I0VtzsRbekxEutHSme7NPS2qE8StCYQNUnfWdXta/Yu85A==", 282 | "engines": { 283 | "node": ">=8.3.0" 284 | }, 285 | "peerDependencies": { 286 | "bufferutil": "^4.0.1", 287 | "utf-8-validate": "^5.0.2" 288 | }, 289 | "peerDependenciesMeta": { 290 | "bufferutil": { 291 | "optional": true 292 | }, 293 | "utf-8-validate": { 294 | "optional": true 295 | } 296 | } 297 | } 298 | }, 299 | "dependencies": { 300 | "async": { 301 | "version": "0.9.2", 302 | "resolved": "https://registry.npmjs.org/async/-/async-0.9.2.tgz", 303 | "integrity": "sha1-rqdNXmHB+JlhO/ZL2mbUx48v0X0=" 304 | }, 305 | "balanced-match": { 306 | "version": "1.0.2", 307 | "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", 308 | "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==" 309 | }, 310 | "brace-expansion": { 311 | "version": "1.1.11", 312 | "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", 313 | "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", 314 | "requires": { 315 | "balanced-match": "^1.0.0", 316 | "concat-map": "0.0.1" 317 | } 318 | }, 319 | "colors": { 320 | "version": "1.4.0", 321 | "resolved": "https://registry.npmjs.org/colors/-/colors-1.4.0.tgz", 322 | "integrity": "sha512-a+UqTh4kgZg/SlGvfbzDHpgRu7AAQOmmqRHJnxhRZICKFUT91brVhNNt58CMWU9PsBbv3PDCZUHbVxuDiH2mtA==" 323 | }, 324 | "concat-map": { 325 | "version": "0.0.1", 326 | "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", 327 | "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=" 328 | }, 329 | "cycle": { 330 | "version": "1.0.3", 331 | "resolved": "https://registry.npmjs.org/cycle/-/cycle-1.0.3.tgz", 332 | "integrity": "sha1-IegLK+hYD5i0aPN5QwZisEbDStI=" 333 | }, 334 | "deep-equal": { 335 | "version": "0.2.2", 336 | "resolved": "https://registry.npmjs.org/deep-equal/-/deep-equal-0.2.2.tgz", 337 | "integrity": "sha1-hLdFiW80xoTpjyzg5Cq69Du6AX0=" 338 | }, 339 | "eyes": { 340 | "version": "0.1.8", 341 | "resolved": "https://registry.npmjs.org/eyes/-/eyes-0.1.8.tgz", 342 | "integrity": "sha1-Ys8SAjTGg3hdkCNIqADvPgzCC8A=" 343 | }, 344 | "fs.realpath": { 345 | "version": "1.0.0", 346 | "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", 347 | "integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8=" 348 | }, 349 | "glob": { 350 | "version": "7.1.7", 351 | "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.7.tgz", 352 | "integrity": "sha512-OvD9ENzPLbegENnYP5UUfJIirTg4+XwMWGaQfQTY0JenxNvvIKP3U3/tAQSPIu/lHxXYSZmpXlUHeqAIdKzBLQ==", 353 | "requires": { 354 | "fs.realpath": "^1.0.0", 355 | "inflight": "^1.0.4", 356 | "inherits": "2", 357 | "minimatch": "^3.0.4", 358 | "once": "^1.3.0", 359 | "path-is-absolute": "^1.0.0" 360 | } 361 | }, 362 | "i": { 363 | "version": "0.3.6", 364 | "resolved": "https://registry.npmjs.org/i/-/i-0.3.6.tgz", 365 | "integrity": "sha1-2WyScyB28HJxG2sQ/X1PZa2O4j0=" 366 | }, 367 | "inflight": { 368 | "version": "1.0.6", 369 | "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", 370 | "integrity": "sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=", 371 | "requires": { 372 | "once": "^1.3.0", 373 | "wrappy": "1" 374 | } 375 | }, 376 | "inherits": { 377 | "version": "2.0.4", 378 | "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", 379 | "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==" 380 | }, 381 | "isstream": { 382 | "version": "0.1.2", 383 | "resolved": "https://registry.npmjs.org/isstream/-/isstream-0.1.2.tgz", 384 | "integrity": "sha1-R+Y/evVa+m+S4VAOaQ64uFKcCZo=" 385 | }, 386 | "minimatch": { 387 | "version": "3.0.4", 388 | "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz", 389 | "integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==", 390 | "requires": { 391 | "brace-expansion": "^1.1.7" 392 | } 393 | }, 394 | "minimist": { 395 | "version": "1.2.5", 396 | "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.5.tgz", 397 | "integrity": "sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw==" 398 | }, 399 | "mkdirp": { 400 | "version": "0.5.5", 401 | "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.5.tgz", 402 | "integrity": "sha512-NKmAlESf6jMGym1++R0Ra7wvhV+wFW63FaSOFPwRahvea0gMUcGUhVeAg/0BC0wiv9ih5NYPB1Wn1UEI1/L+xQ==", 403 | "requires": { 404 | "minimist": "^1.2.5" 405 | } 406 | }, 407 | "mute-stream": { 408 | "version": "0.0.8", 409 | "resolved": "https://registry.npmjs.org/mute-stream/-/mute-stream-0.0.8.tgz", 410 | "integrity": "sha512-nnbWWOkoWyUsTjKrhgD0dcz22mdkSnpYqbEjIm2nhwhuxlSkpywJmBo8h0ZqJdkp73mb90SssHkN4rsRaBAfAA==" 411 | }, 412 | "ncp": { 413 | "version": "1.0.1", 414 | "resolved": "https://registry.npmjs.org/ncp/-/ncp-1.0.1.tgz", 415 | "integrity": "sha1-0VNn5cuHQyuhF9K/gP30Wuz7QkY=" 416 | }, 417 | "once": { 418 | "version": "1.4.0", 419 | "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", 420 | "integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=", 421 | "requires": { 422 | "wrappy": "1" 423 | } 424 | }, 425 | "path-is-absolute": { 426 | "version": "1.0.1", 427 | "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", 428 | "integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18=" 429 | }, 430 | "prompt": { 431 | "version": "1.1.0", 432 | "resolved": "https://registry.npmjs.org/prompt/-/prompt-1.1.0.tgz", 433 | "integrity": "sha512-ec1vUPXCplDBDUVD8uPa3XGA+OzLrO40Vxv3F1uxoiZGkZhdctlK2JotcHq5X6ExjocDOGwGdCSXloGNyU5L1Q==", 434 | "requires": { 435 | "colors": "^1.1.2", 436 | "read": "1.0.x", 437 | "revalidator": "0.1.x", 438 | "utile": "0.3.x", 439 | "winston": "2.x" 440 | } 441 | }, 442 | "read": { 443 | "version": "1.0.7", 444 | "resolved": "https://registry.npmjs.org/read/-/read-1.0.7.tgz", 445 | "integrity": "sha1-s9oZvQUkMal2cdRKQmNK33ELQMQ=", 446 | "requires": { 447 | "mute-stream": "~0.0.4" 448 | } 449 | }, 450 | "revalidator": { 451 | "version": "0.1.8", 452 | "resolved": "https://registry.npmjs.org/revalidator/-/revalidator-0.1.8.tgz", 453 | "integrity": "sha1-/s5hv6DBtSoga9axgZgYS91SOjs=" 454 | }, 455 | "rimraf": { 456 | "version": "2.7.1", 457 | "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.7.1.tgz", 458 | "integrity": "sha512-uWjbaKIK3T1OSVptzX7Nl6PvQ3qAGtKEtVRjRuazjfL3Bx5eI409VZSqgND+4UNnmzLVdPj9FqFJNPqBZFve4w==", 459 | "requires": { 460 | "glob": "^7.1.3" 461 | } 462 | }, 463 | "stack-trace": { 464 | "version": "0.0.10", 465 | "resolved": "https://registry.npmjs.org/stack-trace/-/stack-trace-0.0.10.tgz", 466 | "integrity": "sha1-VHxws0fo0ytOEI6hoqFZ5f3eGcA=" 467 | }, 468 | "utile": { 469 | "version": "0.3.0", 470 | "resolved": "https://registry.npmjs.org/utile/-/utile-0.3.0.tgz", 471 | "integrity": "sha1-E1LDQOuCDk2N26A5pPv6oy7U7zo=", 472 | "requires": { 473 | "async": "~0.9.0", 474 | "deep-equal": "~0.2.1", 475 | "i": "0.3.x", 476 | "mkdirp": "0.x.x", 477 | "ncp": "1.0.x", 478 | "rimraf": "2.x.x" 479 | } 480 | }, 481 | "winston": { 482 | "version": "2.4.5", 483 | "resolved": "https://registry.npmjs.org/winston/-/winston-2.4.5.tgz", 484 | "integrity": "sha512-TWoamHt5yYvsMarGlGEQE59SbJHqGsZV8/lwC+iCcGeAe0vUaOh+Lv6SYM17ouzC/a/LB1/hz/7sxFBtlu1l4A==", 485 | "requires": { 486 | "async": "~1.0.0", 487 | "colors": "1.0.x", 488 | "cycle": "1.0.x", 489 | "eyes": "0.1.x", 490 | "isstream": "0.1.x", 491 | "stack-trace": "0.0.x" 492 | }, 493 | "dependencies": { 494 | "async": { 495 | "version": "1.0.0", 496 | "resolved": "https://registry.npmjs.org/async/-/async-1.0.0.tgz", 497 | "integrity": "sha1-+PwEyjoTeErenhZBr5hXjPvWR6k=" 498 | }, 499 | "colors": { 500 | "version": "1.0.3", 501 | "resolved": "https://registry.npmjs.org/colors/-/colors-1.0.3.tgz", 502 | "integrity": "sha1-BDP0TYCWgP3rYO0mDxsMJi6CpAs=" 503 | } 504 | } 505 | }, 506 | "wrappy": { 507 | "version": "1.0.2", 508 | "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", 509 | "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=" 510 | }, 511 | "ws": { 512 | "version": "7.4.6", 513 | "resolved": "https://registry.npmjs.org/ws/-/ws-7.4.6.tgz", 514 | "integrity": "sha512-YmhHDO4MzaDLB+M9ym/mDA5z0naX8j7SIlT8f8z+I0VtzsRbekxEutHSme7NPS2qE8StCYQNUnfWdXta/Yu85A==", 515 | "requires": {} 516 | } 517 | } 518 | } 519 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | --------------------------------------------------------------------------------