├── .gitignore ├── README.md ├── index.js ├── package-lock.json ├── package.json ├── src ├── Client.js ├── Struct │ ├── AuthorizedUser.js │ ├── Backpack │ │ ├── Backpack.js │ │ ├── BackpackImage.js │ │ ├── BackpackItem.js │ │ ├── BackpackScript.js │ │ └── BackpackSprite.js │ ├── CloudSession.js │ ├── CloudVariable.js │ ├── CommentAuthor.js │ ├── Image.js │ ├── IncompleteUser.js │ ├── News.js │ ├── Permission.js │ ├── Project.js │ ├── ProjectComment.js │ ├── ScratchAsset.js │ ├── ScratchImageAsset.js │ ├── ScratchSoundAsset.js │ ├── Session.js │ ├── SessionFlags.js │ ├── Studio.js │ ├── User.js │ ├── UserComment.js │ ├── UserFlag.js │ ├── UserProfile.js │ └── _StudioComment.js └── request.js └── test ├── cloud.js └── index.js /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # node-scratch-client 2 | 3 | This package is a nodejs promise-based client to connect with the Scratch web and cloud servers. This package is based off https://github.com/trumank/scratch-api which the developer has discontinued. 4 | 5 | Full project manipulation: 6 | ```js 7 | // Import the module with: 8 | const scratch = require("node-scratch-client"); 9 | 10 | // Initiate client 11 | const Client = new scratch.Client({ 12 | username: "griffpatch", 13 | password: "SecurePassword1" 14 | }); 15 | 16 | // Login 17 | Client.login().then(() => { 18 | // Fetch project information 19 | Client.getProject(10128407).then(project => { 20 | project.postComment("Turning off comments.."); 21 | 22 | project.turnOffCommenting(); 23 | }); 24 | }); 25 | ``` 26 | 27 | Cloud server connection: 28 | ```js 29 | const scratch = require("node-scratch-client"); 30 | 31 | const Client = new scratch.Client({ 32 | username: "ceebeee", 33 | password: "SecurePassword2" 34 | }); 35 | 36 | Client.login().then(() => { 37 | let cloud = Client.session.createCloudSession(281983597); 38 | 39 | cloud.connect().then(() => { 40 | cloud.on("set", variable => { 41 | console.log("Variable changed to " + variable.value); 42 | }); 43 | }); 44 | }); 45 | ``` 46 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | Client: require("./src/Client.js") 3 | } 4 | -------------------------------------------------------------------------------- /package-lock.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "node-scratch-client", 3 | "version": "1.3.7", 4 | "lockfileVersion": 1, 5 | "requires": true, 6 | "dependencies": { 7 | "cookie": { 8 | "version": "0.3.1", 9 | "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.3.1.tgz", 10 | "integrity": "sha1-5+Ch+e9DtMi6klxcWpboBtFoc7s=" 11 | }, 12 | "dotenv": { 13 | "version": "7.0.0", 14 | "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-7.0.0.tgz", 15 | "integrity": "sha512-M3NhsLbV1i6HuGzBUH8vXrtxOk+tWmzWKDMbAVSUp3Zsjm7ywFeuwrUXhmhQyRK1q5B5GGy7hcXPbj3bnfZg2g==" 16 | }, 17 | "querystring": { 18 | "version": "0.2.0", 19 | "resolved": "https://registry.npmjs.org/querystring/-/querystring-0.2.0.tgz", 20 | "integrity": "sha1-sgmEkgO7Jd+CDadW50cAWHhSFiA=" 21 | }, 22 | "ws": { 23 | "version": "7.4.6", 24 | "resolved": "https://registry.npmjs.org/ws/-/ws-7.4.6.tgz", 25 | "integrity": "sha512-YmhHDO4MzaDLB+M9ym/mDA5z0naX8j7SIlT8f8z+I0VtzsRbekxEutHSme7NPS2qE8StCYQNUnfWdXta/Yu85A==" 26 | } 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "node-scratch-client", 3 | "version": "1.3.7", 4 | "description": "A nodejs client to interact with the scratch website.", 5 | "main": "index.js", 6 | "directories": { 7 | "test": "test" 8 | }, 9 | "scripts": { 10 | "test": "echo \"Error: no test specified\" && exit 1" 11 | }, 12 | "author": "", 13 | "license": "ISC", 14 | "dependencies": { 15 | "cookie": "^0.3.1", 16 | "dotenv": "^7.0.0", 17 | "querystring": "^0.2.0", 18 | "ws": "^7.4.6" 19 | }, 20 | "devDependencies": {}, 21 | "repository": { 22 | "type": "git", 23 | "url": "git+https://github.com/mytopdog/node-scratch-client.git" 24 | }, 25 | "bugs": { 26 | "url": "https://github.com/mytopdog/node-scratch-client/issues" 27 | }, 28 | "homepage": "https://github.com/mytopdog/node-scratch-client#readme" 29 | } 30 | -------------------------------------------------------------------------------- /src/Client.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | const request = require("./request.js"); 4 | const cookie = require("cookie"); 5 | const querystring = require("querystring"); 6 | 7 | const Project = require("./Struct/Project.js"); 8 | const User = require("./Struct/User.js"); 9 | const Studio = require("./Struct/Studio.js"); 10 | const Session = require("./Struct/Session.js"); 11 | const SessionFlags = require("./Struct/SessionFlags.js"); 12 | const News = require("./Struct/News.js"); 13 | 14 | class Client { 15 | constructor(auth, debug) { 16 | this._client = this; 17 | 18 | this.auth = { 19 | username: auth.username || "", 20 | password: auth.password || "" 21 | } 22 | 23 | this.debug = debug || null; 24 | 25 | this.session = {}; 26 | 27 | this.csrf = null; 28 | } 29 | 30 | _fetchcsrf() { 31 | let _this = this; 32 | 33 | return new Promise((resolve, reject) => { 34 | request({ 35 | path: "/csrf_token/" 36 | }, { 37 | "X-Requested-With": "XMLHttpRequest", 38 | "Cookie": "scratchlanguage=en; permissions=%7B%7D;" 39 | }).then(data => { 40 | const cookies = cookie.parse(data.response.headers["set-cookie"][1]); 41 | 42 | _this.csrf = cookies.scratchcsrftoken; 43 | 44 | resolve(cookies.scratchcsrftoken); 45 | }); 46 | }); 47 | } 48 | 49 | _fetchSession() { 50 | let _this = this; 51 | 52 | return new Promise((resolve, reject) => { 53 | if (!_this.session) reject(new Error("No user session found.")); 54 | 55 | request({ 56 | path: "/session/", 57 | sessionid: _this.session.sessionid, 58 | csrftoken: _this.session.csrftoken 59 | }, { 60 | "X-Requested-With": "XMLHttpRequest" 61 | }).then(response => { 62 | const body = response.body; 63 | 64 | let json = JSON.parse(body)[0]; 65 | 66 | _this.session.authorized = new SessionFlags(_this, JSON.parse(body)); 67 | 68 | _this.session.authorized.on("ready", function () { 69 | resolve(_this.session); 70 | }); 71 | }).catch(reject); 72 | }); 73 | } 74 | 75 | login() { 76 | let _this = this; 77 | 78 | return new Promise((resolve, reject) => { 79 | let body = JSON.stringify({ 80 | username: _this.auth.username, 81 | password: _this.auth.password 82 | }); 83 | 84 | _this._fetchcsrf().then(csrf => { 85 | request({ 86 | path: "/login/", 87 | method: "POST", 88 | sessionid: null, 89 | body: body, 90 | csrftoken: csrf 91 | }, { 92 | "X-Requested-With": "XMLHttpRequest", 93 | }).then(data => { 94 | const body = data.body; 95 | const response = data.response; 96 | 97 | let json = JSON.parse(body)[0]; 98 | 99 | if (json.success === 0 && json.msg === "Incorrect username or password.") { 100 | return reject(new Error("Incorrect username or password provided.")); 101 | } 102 | 103 | let cookies = cookie.parse(response.headers["set-cookie"][0]); 104 | 105 | _this.session = new Session(_this, { 106 | sessionid: cookies.scratchsessionsid, 107 | csrftoken: csrf, 108 | id: json.id, 109 | username: _this.auth.username 110 | }); 111 | 112 | _this._fetchSession().then(function () { 113 | _this._debugLog("Logged in. " + json.method); 114 | 115 | resolve(_this.session); 116 | }); 117 | }).catch(reject); 118 | }); 119 | }); 120 | } 121 | 122 | _debugLog(log) { 123 | if (this.debug) { 124 | this.debug(log); 125 | } 126 | } 127 | 128 | getProject(project) { 129 | let _this = this; 130 | 131 | let id = project.id || project; 132 | 133 | return new Promise((resolve, reject) => { 134 | request({ 135 | hostname: "api.scratch.mit.edu", 136 | path: "/projects/" + id, 137 | method: "GET", 138 | sessionid: _this.session.sessionid, 139 | csrftoken: _this.session.csrftoken 140 | }).then(response => { 141 | resolve(new Project(_this, JSON.parse(response.body))); 142 | }).catch(reject); 143 | }); 144 | } 145 | 146 | getProjectCount() { 147 | let _this = this; 148 | 149 | return new Promise((resolve, reject) => { 150 | request({ 151 | hostname: "api.scratch.mit.edu", 152 | path: "/projects/count/all", 153 | method: "GET", 154 | csrftoken: _this.session.csrftoken 155 | }).then(response => { 156 | resolve(JSON.parse(response.body).count); 157 | }).catch(reject); 158 | }); 159 | } 160 | 161 | getUser(user) { 162 | let _this = this; 163 | 164 | let username = user.username || user; 165 | 166 | return new Promise((resolve, reject) => { 167 | request({ 168 | hostname: "api.scratch.mit.edu", 169 | path: "/users/" + username, 170 | method: "GET", 171 | sessionid: _this.session.sessionid, 172 | csrftoken: _this.session.csrftoken 173 | }).then(response => { 174 | resolve(new User(_this, JSON.parse(response.body))); 175 | }).catch(reject); 176 | }); 177 | } 178 | 179 | getStudio(studio) { 180 | let _this = this; 181 | 182 | let id = studio.id || studio; 183 | 184 | return new Promise((resolve, reject) => { 185 | request({ 186 | hostname: "api.scratch.mit.edu", 187 | path: "/studios/" + id, 188 | method: "GET", 189 | sessionid: _this.session.sessionid, 190 | csrftoken: _this.session.csrftoken 191 | }).then(response => { 192 | resolve(new Studio(_this, JSON.parse(response.body))); 193 | }).catch(reject); 194 | }); 195 | } 196 | 197 | exploreProjects(tags, mode) { // under construction 198 | let _this = this; 199 | let query = []; 200 | if (tags) { 201 | query.push(tags); 202 | } 203 | if (mode) { 204 | query.push(mode); 205 | } 206 | query = query.join("&"); 207 | 208 | return new Promise((resolve, reject) => { 209 | request({ 210 | hostname: "api.scratch.mit.edu", 211 | path: "/explore/projects" + (query ? "?" + query : ""), 212 | method: "GET", 213 | sessionid: _this.session.sessionid, 214 | csrftoken: _this.session.csrftoken 215 | }).then(response => { 216 | let projects = JSON.parse(response.body); 217 | 218 | resolve(projects.map(project => { 219 | return new Project(_this, project); 220 | })); 221 | }).catch(reject); 222 | }); 223 | } 224 | 225 | exploreStudios(tags, mode) { // under construction 226 | let _this = this; 227 | let query = []; 228 | if (tags) { 229 | query.push(tags); 230 | } 231 | if (mode) { 232 | query.push(mode); 233 | } 234 | query = query.join("&"); 235 | 236 | return new Promise((resolve, reject) => { 237 | request({ 238 | hostname: "api.scratch.mit.edu", 239 | path: "/explore/studios" + (query ? "?" + query : ""), 240 | method: "GET", 241 | sessionid: _this.session.sessionid, 242 | csrftoken: _this.session.csrftoken 243 | }).then(response => { 244 | let studios = JSON.parse(response.body); 245 | 246 | resolve(studios.map(studio => { 247 | return new Studio(_this, studio); 248 | })); 249 | }).catch(reject); 250 | }); 251 | } 252 | 253 | searchProjects(search, mode) { 254 | let _this = this; 255 | let query = []; 256 | if (search) { 257 | query.push(search); 258 | } 259 | if (mode) { 260 | query.push(mode); 261 | } 262 | query = query.join("&"); 263 | 264 | return new Promise((resolve, reject) => { 265 | request({ 266 | hostname: "api.scratch.mit.edu", 267 | path: "/search/projects" + (query ? "?" + query : ""), 268 | method: "GET", 269 | sessionid: _this.session.sessionid, 270 | csrftoken: _this.session.csrftoken 271 | }).then(response => { 272 | let projects = JSON.parse(response.body); 273 | 274 | resolve(projects.map(project => { 275 | return new Project(_this, project); 276 | })); 277 | }).catch(reject); 278 | }); 279 | } 280 | 281 | searchStudios(search, mode) { 282 | let _this = this; 283 | let query = []; 284 | if (search) { 285 | query.push(search); 286 | } 287 | if (mode) { 288 | query.push(mode); 289 | } 290 | query = query.join("&"); 291 | 292 | return new Promise((resolve, reject) => { 293 | request({ 294 | hostname: "api.scratch.mit.edu", 295 | path: "/search/studios" + (query ? "?" + query : ""), 296 | method: "GET", 297 | sessionid: _this.session.sessionid, 298 | csrftoken: _this.session.csrftoken 299 | }).then(response => { 300 | let studios = JSON.parse(response.body); 301 | 302 | resolve(studios.map(studio => { 303 | return new Studio(_this, studio); 304 | })); 305 | }).catch(reject); 306 | }); 307 | } 308 | 309 | getNews() { 310 | let _this = this; 311 | 312 | return new Promise((resolve, reject) => { 313 | request({ 314 | hostname: "api.scratch.mit.edu", 315 | path: "/news", 316 | method: "GET", 317 | sessionid: _this.session.sessionid, 318 | csrftoken: _this.session.csrftoken 319 | }).then(response => { 320 | let newses = JSON.parse(response.body); // *newses* 321 | 322 | resolve(newses.map(news => { 323 | return new News(_this, news); 324 | })); 325 | }).catch(reject); 326 | }); 327 | } 328 | 329 | isValidUsername(name) { 330 | let _this = this; 331 | 332 | return new Promise((resolve, reject) => { 333 | request({ 334 | path: "/accounts/check_username/" + name, 335 | method: "GET", 336 | sessionid: _this.session.sessionid, 337 | csrftoken: _this.session.csrftoken 338 | }, { 339 | accept: "application/json", 340 | "Content-Type": "application/json", 341 | origin: "https://scratch.mit.edu", 342 | referer: "https://scratch.mit.edu/accounts/standalone-registration/", 343 | "X-Token": _this._client.session.authorized.user.accessToken, 344 | "x-requested-with": "XMLHttpRequest" 345 | }).then(response => { 346 | let isok = JSON.parse(response.body); 347 | 348 | resolve(isok[0].msg === "valid username"); 349 | }).catch(reject); 350 | }); 351 | } 352 | 353 | isValidEmail(email) { 354 | let _this = this; 355 | 356 | return new Promise((resolve, reject) => { 357 | request({ 358 | path: "/accounts/check_email/" + name, 359 | method: "GET", 360 | sessionid: _this.session.sessionid, 361 | csrftoken: _this.session.csrftoken 362 | }, { 363 | accept: "application/json", 364 | "Content-Type": "application/json", 365 | origin: "https://scratch.mit.edu", 366 | referer: "https://scratch.mit.edu/accounts/standalone-registration/", 367 | "X-Token": _this._client.session.authorized.user.accessToken, 368 | "x-requested-with": "XMLHttpRequest" 369 | }).then(response => { 370 | let isok = JSON.parse(response.body); 371 | 372 | resolve(isok[0].msg === "valid email"); 373 | }).catch(reject); 374 | }); 375 | } 376 | 377 | registerNewUser(options) { 378 | let _this = this; 379 | 380 | let opt = options; 381 | 382 | return new Promise((resolve, reject) => { 383 | request({ 384 | path: "/accounts/register_new_user/", 385 | method: "POST", 386 | body: querystring.stringify({ 387 | username: opt.username, 388 | password: opt.password, 389 | birth_month: opt.birthMonth, 390 | birth_year: opt.birthYear, 391 | gender: opt.gender, 392 | country: opt.country, 393 | email: opt.email, 394 | subscribe: opt.subscribe, 395 | is_robot: false, 396 | csrfmiddlewaretoken: _this.csrf 397 | }), 398 | csrftoken: _this.csrf, 399 | }, { 400 | accept: "application/json, text/javascript, */*; q=0.01", 401 | "Content-Type": "application/x-www-form-urlencoded; charset=UTF-8", 402 | origin: "https://scratch.mit.edu", 403 | referer: "https://scratch.mit.edu/accounts/standalone-registration/", 404 | "X-Token": _this._client.session.authorized.user.accessToken, 405 | "x-requested-with": "XMLHttpRequest" 406 | }).then(response => { 407 | let basic = JSON.parse(response.body); 408 | 409 | resolve(basic[0]); 410 | }).catch(reject); 411 | }); 412 | } 413 | 414 | deleteAccount(areYouSure, projects) { 415 | let _this = this; 416 | 417 | if (!areYouSure) return; 418 | 419 | let state = "delbyusr"; 420 | 421 | if (projects) { 422 | state = "delbyusrwproj"; 423 | } 424 | 425 | console.log(querystring.stringify({ 426 | csrfmiddlewaretoken: _this.session.csrftoken, 427 | delete_state: state, 428 | password: _this.auth.password 429 | })); 430 | 431 | return new Promise((resolve, reject) => { 432 | request({ 433 | path: "/accounts/settings/delete_account", 434 | method: "POST", 435 | body: querystring.stringify({ 436 | csrfmiddlewaretoken: _this.session.csrftoken, 437 | delete_state: state, 438 | password: _this.auth.password 439 | }), 440 | sessionid: _this.session.sessionid, 441 | csrftoken: _this.session.csrftoken, 442 | permission: _this.session.permission 443 | }, { 444 | accept: "*/*", 445 | "Content-Type": "application/x-www-form-urlencoded; charset=UTF-8", 446 | "accept-encoding": "gzip, deflate, br", 447 | "accept-language": "en-GB,en-US;q=0.9,en;q=0.8", 448 | origin: "https://scratch.mit.edu", 449 | referer: "https://scratch.mit.edu/accounts/settings/delete_account_confirmation/", 450 | "x-requested-with": "XMLHttpRequest" 451 | }).then(response => { 452 | console.log(response.response.headers); 453 | 454 | resolve(response.request); 455 | }).catch(reject); 456 | }); 457 | } 458 | } 459 | 460 | module.exports = Client; 461 | -------------------------------------------------------------------------------- /src/Struct/AuthorizedUser.js: -------------------------------------------------------------------------------- 1 | const request = require("../request.js"); 2 | const User = require("./User.js"); 3 | const Project = require("./Project.js"); 4 | 5 | class AuthorizedUser extends User { 6 | constructor(Client, raw) { 7 | super(Client, raw); 8 | 9 | this._client = Client; 10 | 11 | this.banned = raw.banned 12 | this.accessToken = raw.token; 13 | this.thumbnailUrl = raw.thumbnailUrl; 14 | this.email = raw.email; 15 | } 16 | 17 | getFollowingRecentStudioProjects() { 18 | let _this = this; 19 | 20 | return new Promise((resolve, reject) => { 21 | request({ 22 | hostname: "api.scratch.mit.edu", 23 | path: "/users/" + _this.username + "/following/studios/projects?x-token=" + _this.accessToken, 24 | method: "GET", 25 | sessionid: _this._client.session.sessionid, 26 | csrftoken: _this._client.session.csrftoken 27 | }).then(response => { 28 | resolve(JSON.parse(response.body).map(project => { 29 | return new Project(project); 30 | })); 31 | }).catch(reject); 32 | }); 33 | } 34 | 35 | getFollowingUserLoves(opt = {}) { 36 | let _this = this; 37 | 38 | return new Promise((resolve, reject) => { 39 | request({ 40 | hostname: "api.scratch.mit.edu", 41 | path: "/users/" + _this.username + "/following/users/loves?x-token=" + _this.accessToken, 42 | method: "GET", 43 | sessionid: _this._client.session.sessionid, 44 | csrftoken: _this._client.session.csrftoken 45 | }).then(response => { 46 | resolve(JSON.parse(response.body).map(project => { 47 | return new Project(project); 48 | })); 49 | }).catch(reject); 50 | }); 51 | } 52 | 53 | getFollowingUserProjects(opt = {}) { 54 | let _this = this; 55 | 56 | return new Promise((resolve, reject) => { 57 | request({ 58 | hostname: "api.scratch.mit.edu", 59 | path: "/users/" + _this.username + "/following/users/projects?x-token=" + _this.accessToken, 60 | method: "GET", 61 | sessionid: _this._client.session.sessionid, 62 | csrftoken: _this._client.session.csrftoken 63 | }).then(response => { 64 | resolve(JSON.parse(response.body).map(project => { 65 | return new Project(project); 66 | })); 67 | }).catch(reject); 68 | }); 69 | } 70 | } 71 | 72 | module.exports = AuthorizedUser; 73 | -------------------------------------------------------------------------------- /src/Struct/Backpack/Backpack.js: -------------------------------------------------------------------------------- 1 | const BackpackImage = require("./BackpackImage.js"); 2 | const BackpackSprite = require("./BackpackSprite.js"); 3 | const BackpackScript = require("./BackpackScript.js"); 4 | 5 | class Backpack { 6 | constructor (Client, raw) { 7 | this._client = Client; 8 | 9 | this.items = []; 10 | 11 | for (let item in raw) { 12 | if (raw[item].type === "script") { 13 | this.items[item] = new BackpackScript(Client, raw[item]); 14 | } else if (raw[item].type === "sprite") { 15 | this.items[item] = new BackpackSprite(Client, raw[item]); 16 | } else if (raw[item].type === "image") { 17 | this.items[item] = new BackpackImage(Client, raw[item]); 18 | } 19 | } 20 | } 21 | } 22 | 23 | module.exports = Backpack; 24 | -------------------------------------------------------------------------------- /src/Struct/Backpack/BackpackImage.js: -------------------------------------------------------------------------------- 1 | const BackpackItem = require("./BackpackItem.js"); 2 | 3 | class BackpackImage extends BackpackItem { 4 | constructor (Client, raw) { 5 | super(Client, raw); 6 | 7 | this.md5 = raw.md5; 8 | } 9 | } 10 | 11 | module.exports = BackpackImage; 12 | -------------------------------------------------------------------------------- /src/Struct/Backpack/BackpackItem.js: -------------------------------------------------------------------------------- 1 | class BackpackItem { 2 | constructor(Client, raw) { 3 | this._client = Client; 4 | 5 | this.type = raw.type; 6 | this.name = raw.name; 7 | } 8 | } 9 | 10 | module.exports = BackpackItem; 11 | -------------------------------------------------------------------------------- /src/Struct/Backpack/BackpackScript.js: -------------------------------------------------------------------------------- 1 | const BackpackItem = require("./BackpackItem.js"); 2 | 3 | class BackpackScript extends BackpackItem { 4 | constructor (Client, raw) { 5 | super(Client, raw); 6 | 7 | this.scripts = raw.scripts 8 | } 9 | } 10 | 11 | module.exports = BackpackScript; 12 | -------------------------------------------------------------------------------- /src/Struct/Backpack/BackpackSprite.js: -------------------------------------------------------------------------------- 1 | const BackpackItem = require("./BackpackItem.js"); 2 | 3 | class BackpackSprite extends BackpackItem { 4 | constructor (Client, raw) { 5 | super(Client, raw); 6 | 7 | this.md5 = raw.md5; 8 | } 9 | } 10 | 11 | module.exports = BackpackSprite; 12 | -------------------------------------------------------------------------------- /src/Struct/CloudSession.js: -------------------------------------------------------------------------------- 1 | const WebSocket = require("ws"); 2 | const EventEmitter = require("events"); 3 | const request = require("../request.js"); 4 | 5 | const CloudVariable = require("./CloudVariable.js"); 6 | 7 | class CloudSession extends EventEmitter { 8 | constructor(Client, user, project) { 9 | super(); 10 | 11 | this._client = Client; 12 | 13 | 14 | this.user = user.username || user; 15 | this.projectid = project.id || project; 16 | this._variables = {} 17 | 18 | this._attemptedPackets = []; 19 | this._connection = null; 20 | } 21 | 22 | resolve(name) { 23 | if (name.startsWith("☁ ")) { 24 | return name; 25 | } else { 26 | return "☁ " + name; 27 | } 28 | } 29 | 30 | getVariable(name) { 31 | return this._variables[name]; 32 | } 33 | 34 | setVariable(name, value) { 35 | this._variables[name].set(value); 36 | } 37 | 38 | _send(method, options) { 39 | let opt = { 40 | user: this.user, 41 | project_id: this.projectid, 42 | method: method, 43 | 44 | ...options 45 | } 46 | 47 | if (this.connection.readyState === WebSocket.OPEN) { 48 | this.connection.send(JSON.stringify(opt) + "\n"); 49 | } else { 50 | this.attemptedPackets.push(JSON.stringify(opt) + "\n"); 51 | } 52 | } 53 | 54 | connect() { 55 | let _this = this; 56 | 57 | return new Promise((resolve, reject) => { 58 | let connection = _this.connection = new WebSocket("wss://clouddata.scratch.mit.edu/", [], { 59 | headers: { 60 | cookie: "scratchsessionsid=" + _this._client.session.sessionid + ";", 61 | origin: "https://scratch.mit.edu" 62 | } 63 | }); 64 | 65 | connection.on("open", function () { 66 | _this._send("handshake", {}); 67 | 68 | for (let i = 0; i < _this._attemptedPackets.length; i++) { 69 | connection._send(_this._attemptedPackets[i]); 70 | } 71 | _this._attemptedPackets = []; 72 | 73 | _this._client._debugLog("Connected to cloud servers"); 74 | 75 | setTimeout(function () { // Leave a small amount of time to receive all variables. 76 | resolve(); 77 | }, 500); 78 | }); 79 | 80 | connection.on("error", reject); 81 | 82 | connection.on("close", function () { 83 | _this.connect(); 84 | }); 85 | 86 | connection.on("message", function (chunk) { 87 | let json = JSON.parse(chunk); 88 | 89 | _this._client._debugLog("CloudData: Received message: " + json); 90 | 91 | if (json.method === "set") { 92 | _this._variables[json.name] = new CloudVariable(_this._client, { 93 | name: json.name, 94 | value: json.value 95 | }); 96 | 97 | _this.emit("set", _this._variables[json.name]); 98 | } else { 99 | _this._client._debugLog("CloudData: Method not supported: " + json.method); 100 | } 101 | }); 102 | }); 103 | } 104 | 105 | stop() { 106 | if (this._connection) { 107 | this._connection.close(); 108 | } 109 | } 110 | } 111 | 112 | module.exports = CloudSession; 113 | -------------------------------------------------------------------------------- /src/Struct/CloudVariable.js: -------------------------------------------------------------------------------- 1 | class CloudVariable { 2 | constructor (Client, raw) { 3 | this._client = Client; 4 | 5 | this.name = raw.name; 6 | this.value = raw.value; 7 | } 8 | 9 | set(value) { 10 | this.value = value; 11 | 12 | this._client.session.cloudsession._send("set", { 13 | name: this.name, 14 | value: this.value 15 | }); 16 | } 17 | } 18 | 19 | module.exports = CloudVariable; 20 | -------------------------------------------------------------------------------- /src/Struct/CommentAuthor.js: -------------------------------------------------------------------------------- 1 | const Image = require("./Image.js"); 2 | 3 | class CommentAuthor { 4 | constructor(Client, raw) { 5 | this._client = Client; 6 | 7 | this.id = raw.id; 8 | this.username = raw.username; 9 | this.scratchteam = raw.scratchteam; 10 | this.avatar = new Image(Client, raw.image); 11 | } 12 | } 13 | 14 | module.exports = CommentAuthor; 15 | -------------------------------------------------------------------------------- /src/Struct/Image.js: -------------------------------------------------------------------------------- 1 | const https = require("https"); 2 | 3 | class Image { 4 | constructor(Client, raw) { 5 | this.src = raw; 6 | } 7 | 8 | download() { 9 | let _this = this; 10 | 11 | return new Promise((resolve, reject) => { 12 | https.get(_this.src, function (response) { 13 | resolve(response); 14 | }); 15 | }); 16 | } 17 | } 18 | 19 | module.exports = Image; 20 | -------------------------------------------------------------------------------- /src/Struct/IncompleteUser.js: -------------------------------------------------------------------------------- 1 | const Image = require("./Image.js"); 2 | 3 | class IncompleteUser { 4 | constructor(Client, raw) { 5 | this._client = Client; 6 | 7 | this.username = raw.username; 8 | this.id = raw.id; 9 | this.scratchteam = raw.scratchteam; 10 | 11 | this.joinedTimestamp = raw.history.joined; 12 | 13 | let avatar = new Image(Client, raw.profile.images["90x90"]); 14 | 15 | this.profile = { 16 | id: raw.profile.id, 17 | avatar: avatar, 18 | avatars: {} 19 | } 20 | 21 | for (let src in raw.profile.images) { 22 | this.profile.avatars[src] = new Image(raw.profile.images[src]); 23 | } 24 | } 25 | } 26 | 27 | module.exports = IncompleteUser; 28 | -------------------------------------------------------------------------------- /src/Struct/News.js: -------------------------------------------------------------------------------- 1 | const Image = require("./Image.js"); 2 | 3 | class News { 4 | constructor (Client, raw) { 5 | this._client = Client; 6 | 7 | this.id = raw.id; 8 | this.timestamp = raw.stamp; 9 | this.title = raw.headline; 10 | this.description = raw.copy; 11 | this.image = new Image(Client, raw.image); 12 | this.src = raw.url; 13 | } 14 | } 15 | 16 | module.exports = News; 17 | -------------------------------------------------------------------------------- /src/Struct/Permission.js: -------------------------------------------------------------------------------- 1 | class Permission { 2 | constructor (Client, name, value) { 3 | this._client = Client; 4 | 5 | this.name = name; 6 | this.value = value; 7 | } 8 | } 9 | 10 | module.exports = Permission; 11 | -------------------------------------------------------------------------------- /src/Struct/Project.js: -------------------------------------------------------------------------------- 1 | const request = require("../request.js"); 2 | const fs = require("fs"); 3 | 4 | const IncompleteUser = require("./IncompleteUser.js"); 5 | const Studio = require("./Studio.js"); 6 | const Image = require("./Image.js"); 7 | const ProjectComment = require("./ProjectComment.js"); 8 | const ScratchImageAsset = require("./ScratchImageAsset.js"); 9 | const ScratchSoundAsset = require("./ScratchSoundAsset.js"); 10 | const ScratchAsset = require("./ScratchAsset.js"); 11 | 12 | class Project { 13 | constructor (Client, raw) { 14 | this._client = Client; 15 | 16 | this.id = raw.id; 17 | this.title = raw.title; 18 | this.description = raw.description; 19 | this.instructions = raw.instructions; 20 | this.visible = raw.visibility === "visible"; 21 | this.public = raw.public; 22 | this.commentsAllowed = raw.comments_allowed; 23 | this.isPublished = raw.is_published; 24 | this.author = new IncompleteUser(Client, raw.author); 25 | 26 | this.thumbnail = new Image(Client, raw.image); 27 | this.thumbnails = {}; 28 | for (let image in raw.images) { 29 | this.thumbnails[image] = new Image(Client, raw.images[image]); 30 | } 31 | this.thumbnails["480x360"] = new Image(Client, raw.image); 32 | 33 | this.createdTimestamp = raw.history.created; 34 | this.lastModifiedTimestamp = raw.history.modified; 35 | this.sharedTimestamp = raw.history.shared; 36 | 37 | this.viewCount = raw.stats.views; 38 | this.loveCount = raw.stats.loves; 39 | this.favoriteCount = raw.stats.favorites; 40 | this.commentCount = raw.stats.comments; 41 | this.remixCount = raw.stats.remixes; 42 | 43 | this.parent = raw.remix.parent || null; 44 | this.root = raw.remix.root || null; 45 | this.isRemix = !!raw.remix.parents; 46 | } 47 | 48 | love() { 49 | let _this = this; 50 | 51 | return new Promise((resolve, reject) => { 52 | request({ 53 | hostname: "api.scratch.mit.edu", 54 | path: "/proxy/projects/" + _this.id + "/loves/user/" + _this._client.session.username, 55 | method: "POST", 56 | sessionid: _this._client.session.sessionid, 57 | csrftoken: _this._client.session.csrftoken 58 | }, { 59 | "X-Requested-With": "XMLHttpRequest", 60 | "X-Token": _this._client.session.authorized.user.accessToken 61 | }).then(response => { 62 | resolve(JSON.parse(response.body).userLove); 63 | }).catch(reject); 64 | }); 65 | } 66 | 67 | unlove() { 68 | let _this = this; 69 | 70 | return new Promise((resolve, reject) => { 71 | request({ 72 | hostname: "api.scratch.mit.edu", 73 | path: "/proxy/projects/" + _this.id + "/loves/user/" + _this._client.session.username, 74 | method: "DELETE", 75 | sessionid: _this._client.session.sessionid, 76 | csrftoken: _this._client.session.csrftoken 77 | }, { 78 | "X-Requested-With": "XMLHttpRequest", 79 | "X-Token": _this._client.session.authorized.user.accessToken 80 | }).then(response => { 81 | resolve(JSON.parse(response.body).userLove); 82 | }).catch(reject); 83 | }); 84 | } 85 | 86 | favorite() { 87 | let _this = this; 88 | 89 | return new Promise((resolve, reject) => { 90 | request({ 91 | hostname: "api.scratch.mit.edu", 92 | path: "/proxy/projects/" + _this.id + "/favorites/user/" + _this._client.session.username, 93 | method: "POST", 94 | sessionid: _this._client.session.sessionid, 95 | csrftoken: _this._client.session.csrftoken 96 | }, { 97 | "X-Requested-With": "XMLHttpRequest", 98 | "X-Token": _this._client.session.authorized.user.accessToken 99 | }).then(response => { 100 | resolve(JSON.parse(response.body).userFavorite); 101 | }).catch(reject); 102 | }); 103 | } 104 | 105 | unfavorite() { 106 | let _this = this; 107 | 108 | return new Promise((resolve, reject) => { 109 | request({ 110 | hostname: "api.scratch.mit.edu", 111 | path: "/proxy/projects/" + _this.id + "/favorites/user/" + _this._client.session.username, 112 | method: "DELETE", 113 | sessionid: _this._client.session.sessionid, 114 | csrftoken: _this._client.session.csrftoken 115 | }, { 116 | "X-Requested-With": "XMLHttpRequest", 117 | "X-Token": _this._client.session.authorized.user.accessToken 118 | }).then(response => { 119 | resolve(JSON.parse(response.body).userFavorite); 120 | }).catch(reject); 121 | }); 122 | } 123 | 124 | getScripts() { 125 | let _this = this; 126 | 127 | return new Promise((resolve, reject) => { 128 | request({ 129 | hostname: "projects.scratch.mit.edu", 130 | path: "/" + _this.id + "/", 131 | method: "GET", 132 | csrftoken: _this._client.session.csrftoken 133 | }).then(response => { 134 | resolve(JSON.parse(response.body)); 135 | }).catch(reject); 136 | }); 137 | } 138 | 139 | getRemixes(opt = {}) { 140 | let _this = this; 141 | let all = []; 142 | 143 | if (opt.fetchAll) { 144 | return new Promise((resolve, reject) => { 145 | (function loop(rCount) { 146 | let query = "limit=40&offset=" + rCount; 147 | 148 | request({ 149 | hostname: "api.scratch.mit.edu", 150 | path: "/users/projects/remixes?" + query, 151 | method: "GET", 152 | csrftoken: _this._client.session.csrftoken 153 | }).then(response => { 154 | let json = JSON.parse(response.body); 155 | 156 | JSON.parse(response.body).forEach(project => { 157 | all.push(new Project(_this._client, project)); 158 | }); 159 | 160 | if (json.length === 40) { 161 | loop(rCount + 40); 162 | } else { 163 | resolve(all); 164 | } 165 | }).catch(reject); 166 | })(0); 167 | }); 168 | } else { 169 | return new Promise((resolve, reject) => { 170 | let query = "limit=" + (opt.limit || 20) + "&offset=" + (opt.offset || 0); 171 | 172 | request({ 173 | hostname: "api.scratch.mit.edu", 174 | path: "/projects/" + _this.id + "/remixes/?" + query, 175 | method: "GET", 176 | csrftoken: _this._client.session.csrftoken 177 | }).then(response => { 178 | resolve(JSON.parse(response.body).map(project => { 179 | return new Project(project); 180 | })); 181 | }).catch(reject); 182 | }); 183 | } 184 | } 185 | 186 | getStudios(opt = {}) { 187 | let _this = this; 188 | let all = []; 189 | 190 | if (opt.fetchAll) { 191 | return new Promise((resolve, reject) => { 192 | (function loop(rCount) { 193 | let query = "limit=40&offset=" + rCount; 194 | 195 | request({ 196 | hostname: "api.scratch.mit.edu", 197 | path: "/projects/" + _this.id + "/studios?" + query, 198 | method: "GET", 199 | csrftoken: _this._client.session.csrftoken 200 | }).then(response => { 201 | let json = JSON.parse(response.body); 202 | 203 | JSON.parse(response.body).forEach(studio => { 204 | all.push(new Studio(_this._client, studio)); 205 | }); 206 | 207 | if (json.length === 40) { 208 | loop(rCount + 40); 209 | } else { 210 | resolve(all); 211 | } 212 | }).catch(reject); 213 | })(0); 214 | }); 215 | } else { 216 | return new Promise((resolve, reject) => { 217 | let query = "limit=" + (opt.limit || 20) + "&offset=" + (opt.offset || 0); 218 | 219 | request({ 220 | hostname: "api.scratch.mit.edu", 221 | path: "/projects/" + _this.id + "/studios/?" + query, 222 | method: "GET", 223 | csrftoken: _this._client.session.csrftoken 224 | }).then(response => { 225 | resolve(JSON.parse(response.body).map(studio => { 226 | return new Studio(_this._client, studio); 227 | })); 228 | }).catch(reject); 229 | }); 230 | } 231 | } 232 | 233 | postComment(content, parent) { 234 | let _this = this; 235 | 236 | return new Promise((resolve, reject) => { 237 | request({ 238 | hostname: "api.scratch.mit.edu", 239 | path: "/proxy/comments/project/" + _this.id + "/", 240 | method: "POST", 241 | body: JSON.stringify({ 242 | commentee_id: "", 243 | content: content, 244 | parent_id: parent || "" 245 | }), 246 | sessionid: _this._client.session.sessionid, 247 | csrftoken: _this._client.session.csrftoken 248 | }, { 249 | accept: "application/json", 250 | "Content-Type": "application/json", 251 | origin: "https://scratch.mit.edu", 252 | referer: "https://scratch.mit.edu/projects/" + _this.id + "/", 253 | "X-Token": _this._client.session.authorized.user.accessToken 254 | }).then(response => { 255 | resolve(); 256 | }).catch(reject); 257 | }); 258 | } 259 | 260 | getComments(opt = {}) { 261 | let _this = this; 262 | let all = []; 263 | 264 | if (opt.fetchAll) { 265 | return new Promise((resolve, reject) => { 266 | (function loop(rCount) { 267 | let query = "limit=40&offset=" + rCount; 268 | 269 | request({ 270 | hostname: "api.scratch.mit.edu", 271 | path: "/projects/" + _this.id + "/comments?" + query, 272 | method: "GET", 273 | csrftoken: _this._client.session.csrftoken 274 | }).then(response => { 275 | let json = JSON.parse(response.body); 276 | 277 | JSON.parse(response.body).forEach(comment => { 278 | all.push(new Comment(_this._client, _this, comment)); 279 | }); 280 | 281 | if (json.length === 40) { 282 | loop(rCount + 40); 283 | } else { 284 | resolve(all); 285 | } 286 | }).catch(reject); 287 | })(0); 288 | }); 289 | } else { 290 | return new Promise((resolve, reject) => { 291 | let query = "limit=" + (opt.limit || 20) + "&offset=" + (opt.offset || 0); 292 | 293 | request({ 294 | hostname: "api.scratch.mit.edu", 295 | path: "/projects/" + _this.id + "/comments/?" + query, 296 | method: "GET", 297 | csrftoken: _this._client.session.csrftoken 298 | }).then(response => { 299 | resolve(JSON.parse(response.body).map(comment => { 300 | return new ProjectComment(comment); 301 | })); 302 | }).catch(reject); 303 | }); 304 | } 305 | } 306 | 307 | turnOffCommenting() { 308 | let _this = this; 309 | 310 | return new Promise((resolve, reject) => { 311 | request({ 312 | hostname: "api.scratch.mit.edu", 313 | path: "/projects/" + _this.id + "/", 314 | method: "PUT", 315 | body: JSON.stringify({ 316 | comments_allowed: false 317 | }), 318 | sessionid: _this._client.session.sessionid, 319 | csrftoken: _this._client.session.csrftoken 320 | }, { 321 | accept: "application/json", 322 | "Content-Type": "application/json", 323 | origin: "https://scratch.mit.edu", 324 | referer: "https://scratch.mit.edu/projects/" + _this.id + "/", 325 | "X-Token": _this._client.session.authorized.user.accessToken 326 | }).then(response => { 327 | _this.commentsAllowed = false; 328 | 329 | resolve(new Project(_this._client, JSON.parse(response.body))); 330 | }).catch(reject); 331 | }); 332 | } 333 | 334 | turnOnCommenting() { 335 | let _this = this; 336 | 337 | return new Promise((resolve, reject) => { 338 | request({ 339 | hostname: "api.scratch.mit.edu", 340 | path: "/projects/" + _this.id + "/", 341 | method: "PUT", 342 | body: JSON.stringify({ 343 | comments_allowed: true 344 | }), 345 | sessionid: _this._client.session.sessionid, 346 | csrftoken: _this._client.session.csrftoken 347 | }, { 348 | accept: "application/json", 349 | "Content-Type": "application/json", 350 | origin: "https://scratch.mit.edu", 351 | referer: "https://scratch.mit.edu/projects/" + _this.id + "/", 352 | "X-Token": _this._client.session.authorized.user.accessToken 353 | }).then(response => { 354 | _this.commentsAllowed = true; 355 | 356 | resolve(new Project(_this._client, JSON.parse(response.body))); 357 | }).catch(reject); 358 | }); 359 | } 360 | 361 | toggleCommenting() { 362 | let _this = this; 363 | 364 | return new Promise((resolve, reject) => { 365 | request({ 366 | hostname: "api.scratch.mit.edu", 367 | path: "/projects/" + _this.id + "/", 368 | method: "PUT", 369 | body: JSON.stringify({ 370 | comments_allowed: !_this.commentsAllowed 371 | }), 372 | sessionid: _this._client.session.sessionid, 373 | csrftoken: _this._client.session.csrftoken 374 | }, { 375 | accept: "application/json", 376 | "Content-Type": "application/json", 377 | origin: "https://scratch.mit.edu", 378 | referer: "https://scratch.mit.edu/projects/" + _this.id + "/", 379 | "X-Token": _this._client.session.authorized.user.accessToken 380 | }).then(response => { 381 | _this.commentsAllowed = !_this.commentsAllowed; 382 | 383 | resolve(new Project(_this._client, JSON.parse(response.body))); 384 | }).catch(reject); 385 | }); 386 | } 387 | 388 | report(category, reason, image) { 389 | let _this = this; 390 | 391 | return new Promise((resolve, reject) => { 392 | request({ 393 | hostname: "api.scratch.mit.edu", 394 | path: "/proxy/comments/project/" + _this.id + "/", 395 | method: "POST", 396 | body: JSON.stringify({ 397 | notes: reason, 398 | report_category: category, 399 | thumbnail: image || _this.project.thumbnail.src 400 | }), 401 | sessionid: _this._client.session.sessionid, 402 | csrftoken: _this._client.session.csrftoken 403 | }, { 404 | accept: "application/json", 405 | "Content-Type": "application/json", 406 | origin: "https://scratch.mit.edu", 407 | referer: "https://scratch.mit.edu/projects/" + _this.id + "/", 408 | "X-Token": _this._client.session.authorized.user.accessToken 409 | }).then(response => { 410 | resolve(response.body); 411 | }).catch(reject); 412 | }); 413 | } 414 | 415 | getAllAssets() { 416 | let _this = this; 417 | let all = []; 418 | 419 | return new Promise((resolve, reject) => { 420 | this.getScripts().then(scripts => { 421 | scripts.targets.forEach(sprite => { 422 | sprite.costumes.forEach(costume => { 423 | all.push(new ScratchImageAsset(_this._client, costume)); 424 | }); 425 | 426 | sprite.sounds.forEach(costume => { 427 | all.push(new ScratchSoundAsset(_this._client, costume)); 428 | }); 429 | 430 | resolve(all); 431 | }) 432 | }).catch(reject); 433 | }); 434 | } 435 | 436 | unshare() { 437 | let _this = this; 438 | 439 | return new Promise((resolve, reject) => { 440 | request({ 441 | hostname: "api.scratch.mit.edu", 442 | path: "/proxy/projects/" + _this.id + "/unshare/", 443 | method: "PUT", 444 | sessionid: _this._client.session.sessionid, 445 | csrftoken: _this._client.session.csrftoken 446 | }, { 447 | accept: "application/json", 448 | "Content-Type": "application/json", 449 | origin: "https://scratch.mit.edu", 450 | referer: "https://scratch.mit.edu/projects/" + _this.id + "/", 451 | "X-Token": _this._client.session.authorized.user.accessToken 452 | }).then(response => { 453 | resolve(); 454 | }).catch(reject); 455 | }); 456 | } 457 | 458 | share() { 459 | let _this = this; 460 | 461 | return new Promise((resolve, reject) => { 462 | request({ 463 | hostname: "api.scratch.mit.edu", 464 | path: "/proxy/projects/" + _this.id + "/share/", 465 | method: "PUT", 466 | sessionid: _this._client.session.sessionid, 467 | csrftoken: _this._client.session.csrftoken 468 | }, { 469 | accept: "application/json", 470 | "Content-Type": "application/json", 471 | origin: "https://scratch.mit.edu", 472 | referer: "https://scratch.mit.edu/projects/" + _this.id + "/", 473 | "X-Token": _this._client.session.authorized.user.accessToken 474 | }).then(response => { 475 | resolve(); 476 | }).catch(reject); 477 | }); 478 | } 479 | 480 | setThumbnail(file) { 481 | let _this = this; 482 | 483 | return new Promise((resolve, reject) => { 484 | request({ 485 | hostname: "scratch.mit.edu", 486 | path: "/internalapi/project/thumbnail/" + _this.id + "/set/", 487 | method: "POST", 488 | body: fs.readFileSync(file), 489 | sessionid: _this._client.session.sessionid, 490 | csrftoken: _this._client.session.csrftoken 491 | }, { 492 | accept: "*/*", 493 | "Content-Type": "image/" + file.split(".")[file.split(".").length - 1], 494 | origin: "https://scratch.mit.edu", 495 | referer: "https://scratch.mit.edu/projects/" + _this.id + "/", 496 | "X-Token": _this._client.session.authorized.user.accessToken, 497 | "x-requested-with": "XMLHttpRequest" 498 | }).then(response => { 499 | resolve(); 500 | }).catch(reject); 501 | }); 502 | } 503 | 504 | view() { 505 | let _this = this; 506 | 507 | return new Promise((resolve, reject) => { 508 | request({ 509 | hostname: "scratch.mit.edu", 510 | path: "/users/" + _this.author.username + "/" + _this.id + "/views/", 511 | method: "POST", 512 | sessionid: _this._client.session.sessionid, 513 | csrftoken: _this._client.session.csrftoken 514 | }, { 515 | accept: "*/*", 516 | "Content-Type": "image/" + file.split(".")[file.split(".").length - 1], 517 | origin: "https://scratch.mit.edu", 518 | referer: "https://scratch.mit.edu/projects/" + _this.id + "/", 519 | "X-Token": _this._client.session.authorized.user.accessToken, 520 | "x-requested-with": "XMLHttpRequest" 521 | }).then(response => { 522 | resolve(); 523 | }).catch(reject); 524 | }); 525 | } 526 | } 527 | 528 | module.exports = Project; 529 | -------------------------------------------------------------------------------- /src/Struct/ProjectComment.js: -------------------------------------------------------------------------------- 1 | const request = require("../request.js"); 2 | 3 | const CommentAuthor = require("./CommentAuthor.js"); 4 | 5 | class ProjectComment { 6 | constructor(Client, project, raw) { 7 | this._client = Client; 8 | 9 | this.id = raw.id; 10 | this.parentid = raw.parent_id; 11 | this.commenteeid = raw.commentee_id; 12 | this.content = raw.content; 13 | this.replyCount = raw.reply_count; 14 | this.project = project; 15 | 16 | this.createdTimestamp = raw.datetime_created; 17 | this.lastModifiedTiemstamp = raw.datetime_modified; 18 | 19 | this.visible = raw.visibility === "visible"; 20 | 21 | this.author = new CommentAuthor(Client, raw.author); 22 | } 23 | 24 | delete() { 25 | let _this = this; 26 | 27 | return new Promise((resolve, reject) => { 28 | request({ 29 | hostname: "api.scratch.mit.edu", 30 | path: "/proxy/comments/project/" + _this.project.id + "/comment/" + _this.id, 31 | method: "DELETE", 32 | sessionid: _this._client.session.sessionid, 33 | csrftoken: _this._client.session.csrftoken 34 | }, { 35 | origin: "https://scratch.mit.edu", 36 | referer: "https://scratch.mit.edu/projects/" + _this.id + "/", 37 | "X-Token": _this._client.session.authorized.user.accessToken 38 | }).then(response => { 39 | resolve(); 40 | }).catch(reject); 41 | }); 42 | } 43 | 44 | report() { 45 | let _this = this; 46 | 47 | return new Promise((resolve, reject) => { 48 | request({ 49 | hostname: "api.scratch.mit.edu", 50 | path: "/proxy/comments/project/" + _this.project.id + "/comment/" + _this.id + "/report", 51 | method: "POST", 52 | sessionid: _this._client.session.sessionid, 53 | csrftoken: _this._client.session.csrftoken 54 | }, { 55 | origin: "https://scratch.mit.edu", 56 | referer: "https://scratch.mit.edu/projects/" + _this.id + "/", 57 | "X-Token": _this._client.session.authorized.user.accessToken 58 | }).then(response => { 59 | resolve(); 60 | }).catch(reject); 61 | }); 62 | } 63 | 64 | reply(content) { 65 | let _this = this; 66 | 67 | return new Promise((resolve, reject) => { 68 | request({ 69 | hostname: "api.scratch.mit.edu", 70 | path: "/proxy/comments/project/" + _this.project.id + "/", 71 | method: "POST", 72 | body: JSON.stringify({ 73 | commentee_id: _this.author.id, 74 | content: content, 75 | parent_id: _this.id 76 | }), 77 | sessionid: _this._client.session.sessionid, 78 | csrftoken: _this._client.session.csrftoken 79 | }, { 80 | accept: "application/json", 81 | "Content-Type": "application/json", 82 | origin: "https://scratch.mit.edu", 83 | referer: "https://scratch.mit.edu/projects/" + _this.id + "/", 84 | "X-Token": _this._client.session.authorized.user.accessToken 85 | }).then(response => { 86 | resolve(response.body); 87 | }).catch(reject); 88 | }); 89 | } 90 | } 91 | 92 | module.exports = ProjectComment; 93 | -------------------------------------------------------------------------------- /src/Struct/ScratchAsset.js: -------------------------------------------------------------------------------- 1 | const http = require("http"); 2 | 3 | class ScratchAsset { 4 | constructor (Client, raw) { 5 | this._client = Client; 6 | 7 | this.assetid = raw.assetId; 8 | this.name = raw.name; 9 | this.dataFormat = raw.dataFormat; 10 | this.md5 = raw.md5ext; 11 | this.src = "http://assets.scratch.mit.edu/internalapi/asset/" + this.md5 + "/get/"; 12 | } 13 | 14 | download() { 15 | let _this = this; 16 | 17 | return new Promise((resolve, reject) => { 18 | http.get(_this.src, function (response) { 19 | resolve(response); 20 | }); 21 | }); 22 | } 23 | } 24 | 25 | module.exports = ScratchAsset; 26 | -------------------------------------------------------------------------------- /src/Struct/ScratchImageAsset.js: -------------------------------------------------------------------------------- 1 | let ScratchAsset = require("./ScratchAsset"); 2 | 3 | class ScatchImageAsset extends ScratchAsset { 4 | constructor (Client, raw) { 5 | super(Client, raw); 6 | 7 | this.bitmapResolution = raw.bitmapResolution; 8 | this.rotationCenterX = raw.rotationCenterX; 9 | this.rotationCenterY = raw.rotationCenterY; 10 | } 11 | } 12 | 13 | module.exports = ScratchAsset; 14 | -------------------------------------------------------------------------------- /src/Struct/ScratchSoundAsset.js: -------------------------------------------------------------------------------- 1 | let ScratchAsset = require("./ScratchAsset"); 2 | 3 | class ScatchSoundAsset extends ScratchAsset { 4 | constructor (Client, raw) { 5 | super(Client, raw); 6 | 7 | this.rate = raw.rate; 8 | this.sampleCount = raw.sampleCount; 9 | } 10 | } 11 | 12 | module.exports = ScratchAsset; 13 | -------------------------------------------------------------------------------- /src/Struct/Session.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | const request = require("../request.js"); 4 | const https = require("https"); 5 | 6 | const Backpack = require("./Backpack/Backpack.js"); 7 | const CloudSession = require("./CloudSession.js"); 8 | 9 | class Session { 10 | constructor(Client, basic) { 11 | this._client = Client; 12 | 13 | this.username = basic.username || null; 14 | this.id = basic.id || null; 15 | this.sessionid = basic.sessionid || null; 16 | this.csrftoken = basic.csrftoken; 17 | 18 | this.authorized = basic.authorized || null; 19 | 20 | this.cloudsession = null; 21 | 22 | this.permission = null; 23 | } 24 | 25 | createCloudSession(project) { 26 | this.cloudsession = new CloudSession(this._client, this.username, project); 27 | 28 | return this.cloudsession; 29 | } 30 | 31 | getBackpack() { 32 | let _this = this; 33 | 34 | return new Promise((resolve, reject) => { 35 | let body = JSON.stringify({ 36 | csrfmiddlewaretoken: "a" 37 | }); 38 | 39 | request({ 40 | path: "/internalapi/backpack/" + _this.username + "/get/", 41 | method: "GET", 42 | sessionid: _this.sessionid, 43 | csrftoken: _this._client.session.csrftoken 44 | }, { 45 | "X-Requested-With": "XMLHttpRequest" 46 | }).then(data => { 47 | const body = data.body; 48 | const response = data.response; 49 | 50 | let json = JSON.parse(body); 51 | 52 | resolve(new Backpack(_this._client, json)); 53 | }).catch(reject); 54 | }); 55 | } 56 | 57 | setBackpack(newBackpack) { 58 | let _this = this; 59 | 60 | return new Promise((resolve, reject) => { 61 | let body = JSON.stringify(newBackpack); 62 | 63 | request({ 64 | hostname: "cdn.scratch.mit.edu", 65 | path: "/internalapi/backpack/" + _this.username + "/set/", 66 | method: "POST", 67 | body: body, 68 | sessionid: _this.sessionid, 69 | csrftoken: _this._client.session.csrftoken 70 | }, {}).then(response => { 71 | resolve(JSON.parse(response.body)); 72 | }).catch(reject); 73 | }); 74 | } 75 | 76 | logout() { // Under construction 77 | let _this = this; 78 | 79 | return new Promise((resolve, reject) => { 80 | request({ 81 | path: "/accounts/logout/", 82 | method: "POST", 83 | body: JSON.stringify({ 84 | csrfmiddlewaretoken: "a" 85 | }), 86 | sessionid: _this._client.session.sessionid, 87 | csrftoken: _this._client.session.csrftoken 88 | }, { 89 | accept: "application/json", 90 | "Content-Type": "application/json", 91 | origin: "https://scratch.mit.edu", 92 | referer: "https://scratch.mit.edu/projects/" + _this.id + "/", 93 | "X-Token": _this._client.session.authorized.user.accessToken 94 | }).then(response => { 95 | resolve(); 96 | }).catch(reject); 97 | }); 98 | } 99 | } 100 | 101 | module.exports = Session; 102 | -------------------------------------------------------------------------------- /src/Struct/SessionFlags.js: -------------------------------------------------------------------------------- 1 | const request = require("../request.js"); 2 | const UserFlag = require("./UserFlag.js"); 3 | const Permission = require("./Permission.js"); 4 | const AuthorizedUser = require("./AuthorizedUser.js"); 5 | const EventEmitter = require("events"); 6 | 7 | class SessionFlags extends EventEmitter { 8 | constructor(Client, raw) { 9 | super(); 10 | 11 | this._client = Client; 12 | 13 | request({ 14 | hostname: "api.scratch.mit.edu", 15 | path: "/users/" + raw.user.username, 16 | method: "GET" 17 | }).then(response => { 18 | let json = JSON.parse(response.body); 19 | 20 | Object.assign(json, { 21 | banned: raw.user.banned, 22 | username: raw.user.username, 23 | token: raw.user.token, 24 | thumbnailUrl: raw.user.thumbnailUrl, 25 | joinedTimestamp: raw.user.dateJoined, 26 | email: raw.user.email 27 | }); 28 | 29 | this.user = new AuthorizedUser(Client, json); 30 | 31 | this.emit("ready"); 32 | }); 33 | 34 | this.permissions = {}; 35 | this.flags = {}; 36 | 37 | Client.session.permission = JSON.stringify(raw.permissions); 38 | 39 | for (let perm in raw.permissions) { 40 | this.permissions[perm] = new Permission(Client, perm, raw.permissions[perm]); 41 | } 42 | 43 | for (let flag in raw.flags) { 44 | this.flags[flag] = new UserFlag(Client, flag, raw.flags[flag]); 45 | } 46 | } 47 | } 48 | 49 | module.exports = SessionFlags; 50 | -------------------------------------------------------------------------------- /src/Struct/Studio.js: -------------------------------------------------------------------------------- 1 | const Image = require("./Image.js"); 2 | const request = require("../request.js"); 3 | 4 | const StudioComment = require("./_StudioComment.js"); 5 | 6 | class Studio { 7 | constructor(Client, raw) { 8 | this._client = Client; 9 | 10 | this.id = raw.id; 11 | this.title = raw.title; 12 | this.owner = raw.owner; 13 | this.description = raw.description; 14 | this.image = new Image(raw.image); 15 | this.visible = raw.visibility === "visible"; 16 | this.openToPublic = raw.open_to_all; 17 | 18 | this.createdTimestamp = raw.history.created; 19 | this.lsatModifiedTimestamp = raw.history.modified; 20 | 21 | this.followerCount = raw.stats.followers; 22 | } 23 | 24 | addProject(project) { 25 | let id = project.id || project; 26 | 27 | let _this = this; 28 | 29 | return new Promise((resolve, reject) => { 30 | request({ 31 | hostname: "api.scratch.mit.edu", 32 | path: "/studios/" + _this.id + "/project/" + id + "/", 33 | method: "POST", 34 | sessionid: _this._client.session.sessionid, 35 | csrftoken: _this._client.session.csrftoken 36 | }, { 37 | accept: "application/json", 38 | "Content-Type": "application/json", 39 | origin: "https://scratch.mit.edu", 40 | referer: "https://scratch.mit.edu/projects/" + id + "/", 41 | "X-Token": _this._client.session.authorized.user.accessToken 42 | }).then(response => { 43 | resolve(); 44 | }).catch(reject); 45 | }); 46 | } 47 | 48 | removeProject(project) { 49 | let id = project.id || project; 50 | 51 | let _this = this; 52 | 53 | return new Promise((resolve, reject) => { 54 | request({ 55 | hostname: "api.scratch.mit.edu", 56 | path: "/studios/" + _this.id + "/project/" + id + "/", 57 | method: "DELETE", 58 | sessionid: _this._client.session.sessionid, 59 | csrftoken: _this._client.session.csrftoken 60 | }, { 61 | accept: "application/json", 62 | "Content-Type": "application/json", 63 | origin: "https://scratch.mit.edu", 64 | referer: "https://scratch.mit.edu/projects/" + id + "/", 65 | "X-Token": _this._client.session.authorized.user.accessToken 66 | }).then(response => { 67 | resolve(); 68 | }).catch(reject); 69 | }); 70 | } 71 | 72 | /* OLD SITE-API - SUBJECT TO CHANGE */ 73 | 74 | openToPublic() { 75 | let _this = this; 76 | 77 | return new Promise((resolve, reject) => { 78 | request({ 79 | path: "/site-api/galleries/" + _this.id + "/mark/open/", 80 | method: "PUT", 81 | sessionid: _this._client.session.sessionid, 82 | csrftoken: _this._client.session.csrftoken 83 | }, { 84 | accept: "application/json", 85 | "Content-Type": "application/json", 86 | origin: "https://scratch.mit.edu", 87 | referer: "https://scratch.mit.edu/studios/" + _this.id, 88 | "X-Token": _this._client.session.authorized.user.accessToken, 89 | "x-requested-with": "XMLHttpRequest" 90 | }).then(response => { 91 | _this.openToPublic = true; 92 | 93 | resolve(); 94 | }).catch(reject); 95 | }); 96 | } 97 | 98 | closeToPublic() { 99 | let _this = this; 100 | 101 | return new Promise((resolve, reject) => { 102 | request({ 103 | path: "/site-api/galleries/" + _this.id + "/mark/closed/", 104 | method: "PUT", 105 | sessionid: _this._client.session.sessionid, 106 | csrftoken: _this._client.session.csrftoken 107 | }, { 108 | accept: "application/json", 109 | "Content-Type": "application/json", 110 | origin: "https://scratch.mit.edu", 111 | referer: "https://scratch.mit.edu/studios/" + _this.id, 112 | "X-Token": _this._client.session.authorized.user.accessToken, 113 | "x-requested-with": "XMLHttpRequest" 114 | }).then(response => { 115 | _this.openToPublic = false; 116 | 117 | resolve(); 118 | }).catch(reject); 119 | }); 120 | } 121 | 122 | follow() { 123 | let _this = this; 124 | 125 | return new Promise((resolve, reject) => { 126 | request({ 127 | path: "/site-api/users/bookmarkers/" + _this.id + "/add/?usernames=" + _this._client.session.username, 128 | method: "PUT", 129 | sessionid: _this._client.session.sessionid, 130 | csrftoken: _this._client.session.csrftoken 131 | }, { 132 | accept: "application/json", 133 | "Content-Type": "application/json", 134 | origin: "https://scratch.mit.edu", 135 | referer: "https://scratch.mit.edu/studios/" + _this.id + "/", 136 | "X-Token": _this._client.session.authorized.user.accessToken, 137 | "x-requested-with": "XMLHttpRequest" 138 | }).then(response => { 139 | resolve(); 140 | }).catch(reject); 141 | }); 142 | } 143 | 144 | unfollow() { 145 | let _this = this; 146 | 147 | return new Promise((resolve, reject) => { 148 | request({ 149 | path: "/site-api/users/bookmarkers/" + _this.id + "/remove/?usernames=" + _this._client.session.username, 150 | method: "PUT", 151 | sessionid: _this._client.session.sessionid, 152 | csrftoken: _this._client.session.csrftoken 153 | }, { 154 | accept: "application/json", 155 | "Content-Type": "application/json", 156 | origin: "https://scratch.mit.edu", 157 | referer: "https://scratch.mit.edu/studios/" + _this.id + "/", 158 | "X-Token": _this._client.session.authorized.user.accessToken, 159 | "x-requested-with": "XMLHttpRequest" 160 | }).then(response => { 161 | console.log(response.body); 162 | 163 | resolve(); 164 | }).catch(reject); 165 | }); 166 | } 167 | 168 | toggleCommenting() { 169 | let _this = this; 170 | 171 | return new Promise((resolve, reject) => { 172 | request({ 173 | path: "/site-api/comments/gallery/" + _this.id + "/toggle-comments/", 174 | method: "POST", 175 | sessionid: _this._client.session.sessionid, 176 | csrftoken: _this._client.session.csrftoken 177 | }, { 178 | accept: "application/json", 179 | "Content-Type": "application/json", 180 | origin: "https://scratch.mit.edu", 181 | referer: "https://scratch.mit.edu/studios/" + _this.id + "/comments/", 182 | "X-Token": _this._client.session.authorized.user.accessToken, 183 | "x-requested-with": "XMLHttpRequest" 184 | }).then(response => { 185 | resolve(); 186 | }).catch(reject); 187 | }); 188 | } 189 | 190 | postComment(content, parentid, commenteeid) { 191 | let _this = this; 192 | 193 | return new Promise((resolve, reject) => { 194 | request({ 195 | path: "/site-api/comments/gallery/" + _this.id + "/add/", 196 | method: "POST", 197 | body: JSON.stringify({ 198 | content: content, 199 | parent_id: parentid || "", 200 | commentee_id: commenteeid || "" 201 | }), 202 | sessionid: _this._client.session.sessionid, 203 | csrftoken: _this._client.session.csrftoken 204 | }, { 205 | accept: "application/json", 206 | "Content-Type": "application/json", 207 | origin: "https://scratch.mit.edu", 208 | referer: "https://scratch.mit.edu/studios/" + _this.id + "/comments/", 209 | "X-Token": _this._client.session.authorized.user.accessToken, 210 | "x-requested-with": "XMLHttpRequest" 211 | }).then(response => { 212 | let id = response.body.match(/data-comment-id="\d+"/g); 213 | if (id) { 214 | id = id[0].match(/\d+/)[0]; 215 | } 216 | 217 | let user = response.body.match(/data-comment-user="\w+"/g); 218 | if (user) { 219 | user = user[0].match(/\w+/g)[3]; 220 | } 221 | 222 | resolve(new StudioComment(_this._client, _this, id, user)); 223 | }).catch(reject); 224 | }); 225 | } 226 | 227 | deleteComment(id) { 228 | let _this = this; 229 | 230 | return new Promise((resolve, reject) => { 231 | request({ 232 | path: "/site-api/comments/gallery/" + _this.id + "/del/", 233 | method: "POST", 234 | body: JSON.stringify({ 235 | id: id 236 | }), 237 | sessionid: _this._client.session.sessionid, 238 | csrftoken: _this._client.session.csrftoken 239 | }, { 240 | accept: "application/json", 241 | "Content-Type": "application/json", 242 | origin: "https://scratch.mit.edu", 243 | referer: "https://scratch.mit.edu/studios/" + _this.id + "/comments/", 244 | "X-Token": _this._client.session.authorized.user.accessToken, 245 | "x-requested-with": "XMLHttpRequest" 246 | }).then(response => { 247 | resolve(); 248 | }).catch(reject); 249 | }); 250 | } 251 | 252 | reportComment(id) { 253 | let _this = this; 254 | 255 | return new Promise((resolve, reject) => { 256 | request({ 257 | path: "/site-api/comments/gallery/" + _this.id + "/rep/", 258 | method: "PUT", 259 | body: JSON.stringify({ 260 | id: id 261 | }), 262 | sessionid: _this._client.session.sessionid, 263 | csrftoken: _this._client.session.csrftoken 264 | }, { 265 | accept: "application/json", 266 | "Content-Type": "application/json", 267 | origin: "https://scratch.mit.edu", 268 | referer: "https://scratch.mit.edu/studios/" + _this.id + "/comments/", 269 | "X-Token": _this._client.session.authorized.user.accessToken, 270 | "x-requested-with": "XMLHttpRequest" 271 | }).then(response => { 272 | resolve(); 273 | }).catch(reject); 274 | }); 275 | } 276 | 277 | inviteCurator(user) { 278 | let _this = this; 279 | 280 | let username = user.username || user; 281 | 282 | return new Promise((resolve, reject) => { 283 | request({ 284 | path: "/site-api/users/curators-in/" + _this.id + "/invite_curator/?usernames=" + username, 285 | method: "PUT", 286 | sessionid: _this._client.session.sessionid, 287 | csrftoken: _this._client.session.csrftoken 288 | }, { 289 | accept: "application/json", 290 | "Content-Type": "application/json", 291 | origin: "https://scratch.mit.edu", 292 | referer: "https://scratch.mit.edu/studios/" + _this.id + "/curators/", 293 | "X-Token": _this._client.session.authorized.user.accessToken, 294 | "x-requested-with": "XMLHttpRequest" 295 | }).then(response => { 296 | resolve(); 297 | }).catch(reject); 298 | }); 299 | } 300 | 301 | acceptCurator() { 302 | let _this = this; 303 | 304 | let username = _this._client.session.username; 305 | 306 | return new Promise((resolve, reject) => { 307 | request({ 308 | path: "/site-api/users/curators-in/" + _this.id + "/add/?usernames=" + username, 309 | method: "PUT", 310 | sessionid: _this._client.session.sessionid, 311 | csrftoken: _this._client.session.csrftoken 312 | }, { 313 | accept: "application/json", 314 | "Content-Type": "application/json", 315 | origin: "https://scratch.mit.edu", 316 | referer: "https://scratch.mit.edu/studios/" + _this.id + "/curators/", 317 | "X-Token": _this._client.session.authorized.user.accessToken, 318 | "x-requested-with": "XMLHttpRequest" 319 | }).then(response => { 320 | resolve(); 321 | }).catch(reject); 322 | }); 323 | } 324 | 325 | promoteCurator(user) { 326 | let _this = this; 327 | 328 | let username = user.username || user; 329 | 330 | return new Promise((resolve, reject) => { 331 | request({ 332 | path: "/site-api/users/curators-in/" + _this.id + "/promote/?usernames=" + username, 333 | method: "PUT", 334 | sessionid: _this._client.session.sessionid, 335 | csrftoken: _this._client.session.csrftoken 336 | }, { 337 | accept: "application/json", 338 | "Content-Type": "application/json", 339 | origin: "https://scratch.mit.edu", 340 | referer: "https://scratch.mit.edu/studios/" + _this.id + "/curators/", 341 | "X-Token": _this._client.session.authorized.user.accessToken, 342 | "x-requested-with": "XMLHttpRequest" 343 | }).then(response => { 344 | resolve(); 345 | }).catch(reject); 346 | }); 347 | } 348 | 349 | setDescription(description) { 350 | let _this = this; 351 | 352 | return new Promise((resolve, reject) => { 353 | request({ 354 | path: "/site-api/galleries/all/" + _this.id + "/", 355 | method: "PUT", 356 | body: JSON.stringify({ 357 | description: description 358 | }), 359 | sessionid: _this._client.session.sessionid, 360 | csrftoken: _this._client.session.csrftoken 361 | }, { 362 | accept: "application/json", 363 | "Content-Type": "application/json", 364 | origin: "https://scratch.mit.edu", 365 | referer: "https://scratch.mit.edu/studios/" + _this.id + "/", 366 | "X-Token": _this._client.session.authorized.user.accessToken, 367 | "x-requested-with": "XMLHttpRequest" 368 | }).then(response => { 369 | _this.description = description; 370 | 371 | resolve(); 372 | }).catch(reject); 373 | }); 374 | } 375 | 376 | setTitle(title) { 377 | let _this = this; 378 | 379 | return new Promise((resolve, reject) => { 380 | request({ 381 | path: "/site-api/galleries/all/" + _this.id + "/", 382 | method: "PUT", 383 | body: JSON.stringify({ 384 | title: title 385 | }), 386 | sessionid: _this._client.session.sessionid, 387 | csrftoken: _this._client.session.csrftoken 388 | }, { 389 | accept: "application/json", 390 | "Content-Type": "application/json", 391 | origin: "https://scratch.mit.edu", 392 | referer: "https://scratch.mit.edu/studios/" + _this.id + "/", 393 | "X-Token": _this._client.session.authorized.user.accessToken, 394 | "x-requested-with": "XMLHttpRequest" 395 | }).then(response => { 396 | _this.title = title; 397 | 398 | resolve(); 399 | }).catch(reject); 400 | }); 401 | } 402 | } 403 | 404 | module.exports = Studio; 405 | -------------------------------------------------------------------------------- /src/Struct/User.js: -------------------------------------------------------------------------------- 1 | const request = require("../request.js"); 2 | const querystring = require("querystring"); 3 | 4 | const Project = require("./Project.js"); 5 | const Studio = require("./Studio.js"); 6 | const UserProfile = require("./UserProfile.js"); 7 | 8 | class User { 9 | constructor(Client, raw) { 10 | this._client = Client; 11 | 12 | this.id = raw.id; 13 | this.username = raw.username; 14 | 15 | this.joinedTimestamp = raw.history.joined; 16 | this.loginTimestamp = raw.history.login; 17 | 18 | this.profile = new UserProfile(Client, this, raw.profile); 19 | } 20 | 21 | getProjects(opt = {}) { 22 | let _this = this; 23 | let all = []; 24 | 25 | if (opt.fetchAll) { 26 | return new Promise((resolve, reject) => { 27 | (function loop(rCount) { 28 | let query = "limit=40&offset=" + rCount; 29 | 30 | request({ 31 | hostname: "api.scratch.mit.edu", 32 | path: "/users/" + _this.username + "/projects/?" + query, 33 | method: "GET", 34 | csrftoken: _this._client.session.csrftoken 35 | }).then(response => { 36 | let json = JSON.parse(response.body); 37 | 38 | JSON.parse(response.body).forEach(project => { 39 | all.push(new Project(_this._client, project)); 40 | }); 41 | 42 | if (json.length === 40) { 43 | loop(rCount + 40); 44 | } else { 45 | resolve(all); 46 | } 47 | }).catch(reject); 48 | })(0); 49 | }); 50 | } else { 51 | let query = "limit=" + (opt.limit || 20) + "&offset=" + (opt.offset || 0); 52 | 53 | return new Promise((resolve, reject) => { 54 | request({ 55 | hostname: "api.scratch.mit.edu", 56 | path: "/users/" + _this.username + "/projects/?" + query, 57 | method: "GET", 58 | csrftoken: _this._client.session.csrftoken 59 | }).then(response => { 60 | resolve(JSON.parse(response.body).map(project => { 61 | return new Project(_this._client, project); 62 | })); 63 | }).catch(reject); 64 | }); 65 | } 66 | } 67 | 68 | getProject(project) { 69 | let _this = this; 70 | let id = project.id || project; 71 | 72 | return new Promise((resolve, reject) => { 73 | request({ 74 | hostname: "api.scratch.mit.edu", 75 | path: "/users/" + _this.username + "/projects/" + id, 76 | method: "GET", 77 | csrftoken: _this._client.session.csrftoken 78 | }).then(response => { 79 | resolve(new Project(_this._client, JSON.parse(response.body))); 80 | }).catch(reject); 81 | }); 82 | } 83 | 84 | getCurating(opt = {}) { 85 | let _this = this; 86 | let all = []; 87 | 88 | if (opt.fetchAll) { 89 | return new Promise((resolve, reject) => { 90 | (function loop(rCount) { 91 | let query = "limit=40&offset=" + rCount; 92 | 93 | request({ 94 | hostname: "api.scratch.mit.edu", 95 | path: "/users/" + _this.username + "/studios/curate?" + query, 96 | method: "GET", 97 | csrftoken: _this._client.session.csrftoken 98 | }).then(response => { 99 | let json = JSON.parse(response.body); 100 | 101 | JSON.parse(response.body).forEach(studio => { 102 | all.push(new Studio(_this._client, studio)); 103 | }); 104 | 105 | if (json.length === 40) { 106 | loop(rCount + 40); 107 | } else { 108 | resolve(all); 109 | } 110 | }).catch(reject); 111 | })(0); 112 | }); 113 | } else { 114 | let query = "limit=" + (opt.limit || 20) + "&offset=" + (opt.offset || 0); 115 | 116 | return new Promise((resolve, reject) => { 117 | request({ 118 | hostname: "api.scratch.mit.edu", 119 | path: "/users/" + _this.username + "/studios/curate?" + query, 120 | method: "GET", 121 | csrftoken: _this._client.session.csrftoken 122 | }).then(response => { 123 | resolve(JSON.parse(response.body).map(studio => { 124 | return new Studio(_this._client, studio); 125 | })); 126 | }).catch(reject); 127 | }); 128 | } 129 | } 130 | 131 | getFavorited(opt = {}) { 132 | let _this = this; 133 | let all = []; 134 | 135 | if (opt.fetchAll) { 136 | return new Promise((resolve, reject) => { 137 | (function loop(rCount) { 138 | let query = "limit=40&offset=" + rCount; 139 | 140 | request({ 141 | hostname: "api.scratch.mit.edu", 142 | path: "/users/" + _this.username + "/favorites?" + query, 143 | method: "GET", 144 | csrftoken: _this._client.session.csrftoken 145 | }).then(response => { 146 | let json = JSON.parse(response.body); 147 | 148 | JSON.parse(response.body).forEach(project => { 149 | all.push(new Project(_this._client, project)); 150 | }); 151 | 152 | if (json.length === 40) { 153 | loop(rCount + 40); 154 | } else { 155 | resolve(all); 156 | } 157 | }).catch(reject); 158 | })(0); 159 | }); 160 | } else { 161 | let query = "limit=" + (opt.limit || 20) + "&offset=" + (opt.offset || 0); 162 | 163 | return new Promise((resolve, reject) => { 164 | request({ 165 | hostname: "api.scratch.mit.edu", 166 | path: "/users/" + _this.username + "/favorites?" + query, 167 | method: "GET", 168 | csrftoken: _this._client.session.csrftoken 169 | }).then(response => { 170 | resolve(JSON.parse(response.body).map(project => { 171 | return new Project(_this._client, project); 172 | })); 173 | }).catch(reject); 174 | }); 175 | } 176 | } 177 | 178 | getFollowers(opt = {}) { 179 | let _this = this; 180 | let all = []; 181 | 182 | if (opt.fetchAll) { 183 | return new Promise((resolve, reject) => { 184 | (function loop(rCount) { 185 | let query = "limit=40&offset=" + rCount; 186 | 187 | request({ 188 | hostname: "api.scratch.mit.edu", 189 | path: "/users/" + _this.username + "/followers/?" + query, 190 | method: "GET", 191 | csrftoken: _this._client.session.csrftoken 192 | }).then(response => { 193 | let json = JSON.parse(response.body); 194 | 195 | JSON.parse(response.body).forEach(user => { 196 | all.push(new User(_this._client, user)); 197 | }); 198 | 199 | if (json.length === 40) { 200 | loop(rCount + 40); 201 | } else { 202 | resolve(all); 203 | } 204 | }).catch(reject); 205 | })(0); 206 | }); 207 | } else { 208 | let query = "limit=" + (opt.limit || 20) + "&offset=" + (opt.offset || 0); 209 | 210 | return new Promise((resolve, reject) => { 211 | request({ 212 | hostname: "api.scratch.mit.edu", 213 | path: "/users/" + _this.username + "/followers?" + query, 214 | method: "GET", 215 | csrftoken: _this._client.session.csrftoken 216 | }).then(response => { 217 | resolve(JSON.parse(response.body).map(user => { 218 | return new User(_this._client, user); 219 | })); 220 | }).catch(reject); 221 | }); 222 | } 223 | } 224 | 225 | getFollowing(opt = {}) { 226 | let _this = this; 227 | let all = []; 228 | 229 | if (opt.fetchAll) { 230 | return new Promise((resolve, reject) => { 231 | (function loop(rCount) { 232 | let query = "limit=40&offset=" + rCount; 233 | 234 | request({ 235 | hostname: "api.scratch.mit.edu", 236 | path: "/users/" + _this.username + "/following?" + query, 237 | method: "GET", 238 | csrftoken: _this._client.session.csrftoken 239 | }).then(response => { 240 | let json = JSON.parse(response.body); 241 | 242 | JSON.parse(response.body).forEach(user => { 243 | all.push(new User(_this._client, user)); 244 | }); 245 | 246 | if (json.length === 40) { 247 | loop(rCount + 40); 248 | } else { 249 | resolve(all); 250 | } 251 | }).catch(reject); 252 | })(0); 253 | }); 254 | } else { 255 | let query = "limit=" + (opt.limit || 20) + "&offset=" + (opt.offset || 0); 256 | 257 | return new Promise((resolve, reject) => { 258 | request({ 259 | hostname: "api.scratch.mit.edu", 260 | path: "/users/" + _this.username + "/following?" + query, 261 | method: "GET", 262 | csrftoken: _this._client.session.csrftoken 263 | }).then(response => { 264 | resolve(JSON.parse(response.body).map(user => { 265 | return new User(_this._client, user); 266 | })); 267 | }).catch(reject); 268 | }); 269 | } 270 | } 271 | 272 | getMessageCount(useOld) { 273 | let _this = this; 274 | 275 | return new Promise((resolve, reject) => { 276 | if (!useOld) { 277 | request({ 278 | hostname: "api.scratch.mit.edu", 279 | path: "/users/" + _this.username + "/messages/count", 280 | method: "GET", 281 | csrftoken: _this._client.session.csrftoken 282 | }).then(response => { 283 | resolve(JSON.parse(response.body).count); 284 | }).catch(reject); 285 | } else { 286 | request({ 287 | hostname: "api.scratch.mit.edu", 288 | path: "/proxy/users/" + _this.username + "/activity/count", 289 | method: "GET", 290 | csrftoken: _this._client.session.csrftoken 291 | }).then(response => { 292 | resolve(JSON.parse(response.body).msg_count); 293 | }).catch(reject); 294 | } 295 | }); 296 | } 297 | 298 | /* OLD SITE-API */ 299 | 300 | postComment(content, parent) { 301 | let _this = this; 302 | 303 | return new Promise((resolve, reject) => { 304 | request({ 305 | path: "/site-api/comments/user/" + _this.username + "/add/", 306 | method: "POST", 307 | body: JSON.stringify({ 308 | commentee_id: "", 309 | content: content, 310 | parent_id: parent || "" 311 | }), 312 | sessionid: _this._client.session.sessionid, 313 | csrftoken: _this._client.session.csrftoken 314 | }, { 315 | accept: "application/json", 316 | "Content-Type": "application/json", 317 | origin: "https://scratch.mit.edu", 318 | referer: "https://scratch.mit.edu/users/" + _this.username + "/", 319 | "X-Token": _this._client.session.authorized.user.accessToken 320 | }).then(response => { 321 | resolve(response.body); 322 | }).catch(reject); 323 | }); 324 | } 325 | 326 | report(field) { 327 | let _this = this; 328 | 329 | // Pick from these fields x 330 | 331 | let fields = [{ 332 | "description": "description", 333 | "working_on": "working_on", 334 | "icon": "icon", 335 | "username": "username", 336 | "aboutme": "description", 337 | "workingon": "working_on", 338 | "avatar": "icon", 339 | "name": "username" 340 | }]; 341 | 342 | return new Promise((resolve, reject) => { 343 | request({ 344 | path: "/site-api/users/all/" + _this.username + "/report/", 345 | method: "POST", 346 | body: JSON.stringify({ 347 | selected_field: fields[field] 348 | }), 349 | sessionid: _this._client.session.sessionid, 350 | csrftoken: _this._client.session.csrftoken 351 | }, { 352 | accept: "application/json", 353 | "Content-Type": "application/json", 354 | origin: "https://scratch.mit.edu", 355 | referer: "https://scratch.mit.edu/users/" + _this.username + "/", 356 | "X-Token": _this._client.session.authorized.user.accessToken 357 | }).then(response => { 358 | resolve(); 359 | }).catch(reject); 360 | }); 361 | } 362 | 363 | toggleCommenting() { 364 | let _this = this; 365 | 366 | return new Promise((resolve, reject) => { 367 | request({ 368 | path: "/site-api/comments/user/" + _this.username + "/toggle-comments/", 369 | method: "POST", 370 | sessionid: _this._client.session.sessionid, 371 | csrftoken: _this._client.session.csrftoken 372 | }, { 373 | accept: "application/json", 374 | "Content-Type": "application/json", 375 | origin: "https://scratch.mit.edu", 376 | referer: "https://scratch.mit.edu/users/" + _this.username + "/", 377 | "X-Token": _this._client.session.authorized.user.accessToken 378 | }).then(response => { 379 | resolve(); 380 | }).catch(reject); 381 | }); 382 | } 383 | 384 | follow() { 385 | let _this = this; 386 | 387 | return new Promise((resolve, reject) => { 388 | request({ 389 | path: "/site-api/users/followers/" + _this.username + "/add/?usernames=" + _this._client.session.username, 390 | method: "PUT", 391 | sessionid: _this._client.session.sessionid, 392 | csrftoken: _this._client.session.csrftoken 393 | }, { 394 | accept: "application/json", 395 | "Content-Type": "application/json", 396 | origin: "https://scratch.mit.edu", 397 | referer: "https://scratch.mit.edu/users/" + _this.username + "/", 398 | "X-Token": _this._client.session.authorized.user.accessToken, 399 | "x-requested-with": "XMLHttpRequest" 400 | }).then(response => { 401 | resolve(); 402 | }).catch(reject); 403 | }); 404 | } 405 | 406 | unfollow() { 407 | let _this = this; 408 | 409 | return new Promise((resolve, reject) => { 410 | request({ 411 | path: "/site-api/users/followers/" + _this.username + "/remove/?usernames=" + _this._client.session.username, 412 | method: "PUT", 413 | sessionid: _this._client.session.sessionid, 414 | csrftoken: _this._client.session.csrftoken 415 | }, { 416 | accept: "application/json", 417 | "Content-Type": "application/json", 418 | origin: "https://scratch.mit.edu", 419 | referer: "https://scratch.mit.edu/users/" + _this.username + "/", 420 | "X-Token": _this._client.session.authorized.user.accessToken, 421 | "x-requested-with": "XMLHttpRequest" 422 | }).then(response => { 423 | resolve(); 424 | }).catch(reject); 425 | }); 426 | } 427 | } 428 | 429 | module.exports = User; 430 | -------------------------------------------------------------------------------- /src/Struct/UserComment.js: -------------------------------------------------------------------------------- 1 | // Still using site-api for User Profiles... 2 | 3 | class UserComment { 4 | 5 | } 6 | -------------------------------------------------------------------------------- /src/Struct/UserFlag.js: -------------------------------------------------------------------------------- 1 | class UserFlag { 2 | constructor (Client, name, value) { 3 | this._client = Client; 4 | 5 | this.name = name; 6 | this.value = value; 7 | } 8 | } 9 | 10 | module.exports = UserFlag; 11 | -------------------------------------------------------------------------------- /src/Struct/UserProfile.js: -------------------------------------------------------------------------------- 1 | const request = require("../request.js"); 2 | 3 | const Image = require("./Image.js"); 4 | 5 | class UserProfile { 6 | constructor(Client, user, raw) { 7 | this._client = Client; 8 | 9 | this.avatar = new Image(Client, raw.images["90x90"]); 10 | this.avatars = {}; 11 | this.id = raw.id; 12 | this.user = user; 13 | this.workingon = raw.status; 14 | this.aboutme = raw.bio; 15 | this.country = raw.country; 16 | 17 | for (let image in raw.images) { 18 | this.avatars[image] = new Image(Client, raw.images[image]); 19 | } 20 | } 21 | 22 | setAboutme(aboutme) { 23 | let _this = this; 24 | 25 | return new Promise((resolve, reject) => { 26 | request({ 27 | path: "/site-api/users/all/" + _this.user.username + "/", 28 | method: "PUT", 29 | body: JSON.stringify({ 30 | bio: aboutme 31 | }), 32 | sessionid: _this._client.session.sessionid, 33 | csrftoken: _this._client.session.csrftoken 34 | }, { 35 | accept: "application/json", 36 | "Content-Type": "application/json", 37 | origin: "https://scratch.mit.edu", 38 | referer: "https://scratch.mit.edu/users/" + _this.user.username + "/", 39 | "X-Token": _this._client.session.authorized.user.accessToken, 40 | "x-requested-with": "XMLHttpRequest" 41 | }).then(response => { 42 | _this.aboutme = aboutme; 43 | 44 | resolve(); 45 | }).catch(reject); 46 | }); 47 | } 48 | 49 | setWorkingon(workingon) { 50 | let _this = this; 51 | 52 | return new Promise((resolve, reject) => { 53 | request({ 54 | path: "/site-api/users/all/" + _this.user.username + "/", 55 | method: "PUT", 56 | body: JSON.stringify({ 57 | status: workingon 58 | }), 59 | sessionid: _this._client.session.sessionid, 60 | csrftoken: _this._client.session.csrftoken 61 | }, { 62 | accept: "application/json", 63 | "Content-Type": "application/json", 64 | origin: "https://scratch.mit.edu", 65 | referer: "https://scratch.mit.edu/users/" + _this.user.username + "/", 66 | "X-Token": _this._client.session.authorized.user.accessToken, 67 | "x-requested-with": "XMLHttpRequest" 68 | }).then(response => { 69 | _this.workingon = workingon; 70 | 71 | resolve(); 72 | }).catch(reject); 73 | }); 74 | } 75 | 76 | setFeaturedProject(type, project) { 77 | type = ({ 78 | "featured_project": "", 79 | "featured_tutorial": 0, 80 | "work_in_progress": 1, 81 | "remix_this": 2, 82 | "my_favorite_things": 3, 83 | "why_i_scratch": 4 84 | })[type]; 85 | let id = project.id || project; 86 | 87 | let _this = this; 88 | 89 | return new Promise((resolve, reject) => { 90 | request({ 91 | path: "/site-api/users/all/" + _this.user.username + "/", 92 | method: "PUT", 93 | body: JSON.stringify({ 94 | featured_project: project, 95 | featured_project_label: type 96 | }), 97 | sessionid: _this._client.session.sessionid, 98 | csrftoken: _this._client.session.csrftoken 99 | }, { 100 | accept: "application/json", 101 | "Content-Type": "application/json", 102 | origin: "https://scratch.mit.edu", 103 | referer: "https://scratch.mit.edu/users/" + _this.user.username + "/", 104 | "X-Token": _this._client.session.authorized.user.accessToken, 105 | "x-requested-with": "XMLHttpRequest" 106 | }).then(response => { 107 | resolve(); 108 | }).catch(reject); 109 | }); 110 | } 111 | } 112 | 113 | module.exports = UserProfile; 114 | -------------------------------------------------------------------------------- /src/Struct/_StudioComment.js: -------------------------------------------------------------------------------- 1 | class StudioComment { 2 | constructor(Client, studio, id, username) { 3 | this._client = Client; 4 | 5 | this.studio = studio; 6 | this.id = Number(id); 7 | this.username = username; 8 | } 9 | 10 | reply(content) { 11 | return new Promise((resolve, reject) => { 12 | this._client.getUser(this.username).then(user => { 13 | this.studio.postComment(content, this.id, user.id) 14 | .then(resolve) 15 | .catch(reject); 16 | }).catch(reject); 17 | }); 18 | } 19 | } 20 | 21 | module.exports = StudioComment; 22 | -------------------------------------------------------------------------------- /src/request.js: -------------------------------------------------------------------------------- 1 | const https = require("https"); 2 | 3 | function requestToScratchServers(opt, headers) { 4 | return new Promise((resolve, reject) => { 5 | let options = { 6 | hostname: opt.hostname || "scratch.mit.edu", 7 | port: 443, 8 | path: opt.path || "/", 9 | method: opt.method || "GET", 10 | headers: { 11 | "Cookie": "scratchcsrftoken=" + (opt.csrftoken || "a") + "; scratchlanguage=en;" + (opt.sessionid ? (" scratchsessionsid=\"" + opt.sessionid + "\"; ") : "") + (opt.permission ? ("permissions=" + encodeURIComponent(opt.permission)) : ""), 12 | "referer": "https://scratch.mit.edu", 13 | "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/73.0.3683.86 Safari/537.36", 14 | "x-csrftoken": (opt.csrftoken || "a"), 15 | 16 | ...headers 17 | }, 18 | } 19 | 20 | if (opt.body) { 21 | options.headers["Content-Length"] = Buffer.byteLength(opt.body); 22 | } 23 | 24 | let req = https.request(options, function (response) { 25 | let res = []; 26 | 27 | response.on("data", function (data) { 28 | res.push(data); 29 | }); 30 | 31 | response.on("end", function () { 32 | let buf = Buffer.concat(res).toString(); 33 | 34 | resolve({ 35 | body: buf, 36 | response: response, 37 | request: options 38 | }); 39 | }); 40 | }); 41 | 42 | req.on("error", reject); 43 | if (opt.body) req.write(opt.body); 44 | req.end(); 45 | }); 46 | } 47 | 48 | module.exports = requestToScratchServers; 49 | -------------------------------------------------------------------------------- /test/cloud.js: -------------------------------------------------------------------------------- 1 | const path = require("path"); 2 | 3 | let scratch = require("../index.js"); 4 | require("dotenv").config({ 5 | path: path.resolve(__dirname, "../../.env") 6 | }); 7 | 8 | let Client = new scratch.Client({ 9 | username: process.env.SCRATCH_USERNAME, 10 | password: process.env.SCRATCH_PASSWORD 11 | }); 12 | 13 | (async _ => { 14 | await Client.login(); 15 | 16 | let cloudSession = Client.session.createCloudSession(299899708); 17 | 18 | cloudSession.connect().then(() => { 19 | cloudSession.on("set", variable => { 20 | setTimeout(() => { 21 | cloudSession.getVariable(cloudSession.resolve("yes")).set(100); 22 | 23 | setTimeout(() => { 24 | variable.set(0); 25 | }, 1000); 26 | }, 1000); 27 | }); 28 | }); 29 | })(); 30 | -------------------------------------------------------------------------------- /test/index.js: -------------------------------------------------------------------------------- 1 | const path = require("path"); 2 | const fs = require("fs"); 3 | 4 | const request = require("../src/request.js"); 5 | 6 | let scratch = require("../index.js"); 7 | require("dotenv").config({ 8 | path: path.resolve(__dirname, "../../.env") 9 | }); 10 | 11 | let Client = new scratch.Client({ 12 | username: process.env.SCRATCH_USERNAME, 13 | password: process.env.SCRATCH_PASSWORD 14 | }); 15 | 16 | 17 | (async _ => { 18 | 19 | })(); 20 | --------------------------------------------------------------------------------