From d5a12d0c71d609adc4e1ed6e5a0811d273f5d97c Mon Sep 17 00:00:00 2001 From: Joshua Lanese Date: Mon, 7 Aug 2017 18:01:18 -0700 Subject: [PATCH 1/6] Add prompts --- examples/cleanWall.js | 171 ++++++++++++++++++++++++++++++------------ 1 file changed, 125 insertions(+), 46 deletions(-) diff --git a/examples/cleanWall.js b/examples/cleanWall.js index b7885ae..6a20e53 100644 --- a/examples/cleanWall.js +++ b/examples/cleanWall.js @@ -1,61 +1,140 @@ // Delete posts from a group wall, including by post content and by author name. var rbx = require('roblox-js'); var ProgressBar = require('progress'); -var username = ''; -var password = ''; -var group = 0; +var prompt = require('prompt'); -rbx.login(username, password) -.then(function () { - // This allows you to retrieve only a specific set of pages. - /* pages = []; - for (var i = 0; i <= 100; i++) { - pages.push(i); - } */ - var wall = new ProgressBar('Getting wall [:bar] :current/:total = :percent :etas remaining ', {total: 10000}); - var promise = rbx.getWall({ - group: group, - // page: pages, - view: true +prompt.message = ''; +var schema = { + properties: { + group: { + description: 'Enter group ID', + required: true, + type: 'integer', + message: 'Group ID must be an integer' + }, + username: { + description: 'Enter ROBLOX account username', + required: true + }, + password: { + description: 'Enter ROBLOX account password', + hidden: true, + replace: '*', + required: true + }, + find: { + description: 'Enter a string to find, this will only delete messages that have the specific string in them (optional)' + }, + author: { + description: 'Enter author username to find. This will only delete messages made by this player (optional)' + }, + startPage: { + description: 'Enter starting page (leave blank for all pages)', + type: 'integer', + message: 'Page must be an integer' + }, + endPage: { + description: 'Enter ending page', + type: 'integer', + message: 'Page must be an integer', + ask: function () { + return prompt.history('startPage').value > 0; + } + } + } +}; + +function clear (group, wall) { + var deletion = new ProgressBar('Deleting posts [:bar] :current/:total = :percent :etas remaining ', {total: 10000}); + console.time('Time: '); + var posts = wall.posts; + var thread = rbx.threaded(function (i) { + var post = posts[i]; + return rbx.deleteWallPost({ + group: group, + post: { + parent: { + index: post.parent.index + }, + view: wall.views[post.parent.page] + } + }); + }, 0, posts.length); + var ivl = setInterval(function () { + deletion.update(thread.getStatus() / 100); + }, 1000); + thread.then(function () { + clearInterval(ivl); + console.timeEnd('Time: '); }); - promise.then(function (wall) { - var posts = wall.posts; - // Remember these are reversed, it starts off with all the posts on the wall and you are REMOVING the ones you DON'T want to delete from the array - /* for (var i = posts.length - 1; i >= 0; i--) { - var post = posts[i]; - if (post.author.name !== 'Bob') { // Delete all posts by Bob - posts.splice(i, 1); +} + +function init (group, username, password, find, author, startPage, endPage) { + rbx.login(username, password) + .then(function () { + var pages; + if (startPage && endPage) { + pages = []; + for (var i = startPage; i <= endPage; i++) { + pages.push(i); + } + } + var wall = new ProgressBar('Getting wall [:bar] :current/:total = :percent :etas remaining ', {total: 10000, clear: true}); + var promise = rbx.getWall({ + group: group, + page: pages, + view: true + }); + promise.then(function (wall) { + var posts = wall.posts; + // Remember these are reversed, it starts off with all the posts on the wall and you are REMOVING the ones you DON'T want to delete from the array + for (var i = posts.length - 1; i >= 0; i--) { + var post = posts[i]; + if (author && post.author.name !== author) { // Delete all posts by Bob + posts.splice(i, 1); + } else if (find && post.content.includes(find)) { // Delete all posts that contain "Bob" + posts.splice(i, 1); + } } - if (!post.content.includes('Bob')) { // Delete all posts that contain "Bob" - posts.splice(i, 1); + if (posts.length === 0) { + console.log('There are no messages to delete!'); + return; } - } */ - var deletion = new ProgressBar('Deleting posts [:bar] :current/:total = :percent :etas remaining ', {total: 10000}); - console.time('Time: '); - var thread = rbx.threaded(function (i) { - var post = posts[i]; - return rbx.deleteWallPost({ - group: group, - post: { - parent: { - index: post.parent.index - }, - view: wall.views[post.parent.page] + console.log('You are about to delete ' + posts.length + ' messages selected from ' + (startPage && endPage ? ('page ' + startPage + ' to ' + endPage) : ('ALL pages'))); + console.log('The list starts from the message starting with "' + posts[0].content.substring(0, 20) + '..." and ends with the message starting with "' + posts[posts.length - 1].content.substring(0, 20) + '..."'); + prompt.get({ + name: 'yesno', + message: 'Are you sure you want to do this? y/n', + validator: /^y|n$/, + required: true, + warning: 'You must respond with "y" or "n"' + }, function (err, result) { + if (err) { + console.error('Prompt error: ' + err.message); + return; + } + if (result.yesno === 'y') { + clear(group, wall); + } else { + console.log('Aborted'); + process.exit(); } }); - }, 0, posts.length); + }); var ivl = setInterval(function () { - deletion.update(thread.getStatus() / 100); + wall.update(promise.getStatus() / 100); }, 1000); - thread.then(function () { + promise.then(function () { clearInterval(ivl); - console.timeEnd('Time: '); }); }); - var ivl = setInterval(function () { - wall.update(promise.getStatus() / 100); - }, 1000); - promise.then(function () { - clearInterval(ivl); - }); +} + +prompt.start(); +prompt.get(schema, function (err, result) { + if (err) { + console.error('Prompt error: ' + err.message); + return; + } + init(result.group, result.username, result.password, result.find, result.author, result.startPage, result.endPage); }); From 2f8675975d23adc698e6e2ce8f45ea2ab42a2fd3 Mon Sep 17 00:00:00 2001 From: Joshua Lanese Date: Fri, 25 Aug 2017 17:48:44 -0700 Subject: [PATCH 2/6] Added streaming Wall will be streamed to a file before being processed with another stream in order to run without huge memory requirements. --- examples/cleanWall.js | 237 ++++++++++++++++++++++++++++++------------ 1 file changed, 172 insertions(+), 65 deletions(-) diff --git a/examples/cleanWall.js b/examples/cleanWall.js index 6a20e53..f3b52e5 100644 --- a/examples/cleanWall.js +++ b/examples/cleanWall.js @@ -2,6 +2,13 @@ var rbx = require('roblox-js'); var ProgressBar = require('progress'); var prompt = require('prompt'); +var stream = require('stream'); +var crypto = require('crypto'); +var fs = require('fs'); +var js = require('JSONStream'); +var mainPath; + +var maxThreads = 5; prompt.message = ''; var schema = { @@ -44,88 +51,175 @@ var schema = { } }; -function clear (group, wall) { - var deletion = new ProgressBar('Deleting posts [:bar] :current/:total = :percent :etas remaining ', {total: 10000}); - console.time('Time: '); - var posts = wall.posts; - var thread = rbx.threaded(function (i) { - var post = posts[i]; - return rbx.deleteWallPost({ +function clean (path) { + console.log('Cleaning up...'); + fs.unlinkSync(path); +} + +function clearPage (group, page) { + var jobs = []; + var indices = page.indices; + for (var i = 0; i < indices.length; i++) { + var index = indices[i]; + jobs.push(rbx.deleteWallPost({ group: group, post: { parent: { - index: post.parent.index + index: index }, - view: wall.views[post.parent.page] + view: page.view } - }); - }, 0, posts.length); + })); + } + return Promise.all(jobs); +} + +function processPage (group, page, author, find) { + var posts = page.posts; + var indices = []; + for (var i = 0; i < posts.length; i++) { + var post = posts[i]; + if (!author || post.author.name === author) { + indices.push(i); + } else if (!find || post.content.includes(find)) { + indices.push(i); + } + } + return { + indices: indices, + view: page.view + }; +} + +function clear (group, path, total) { + var deletePosts = new ProgressBar('Deleting posts [:bar] :current/:total = :percent :etas remaining ', {total: total}); + + var clearStream = new stream.Writable({ + objectMode: true, + highWaterMark: maxThreads + }); + clearStream._write = function (chunk, encoding, done) { + clearPage(group, chunk) + .then(function () { + deletePosts.tick(chunk.indices.length); + }) + .catch(function (err) { + console.error('Clear page error: ' + err.message); + }) + .then(done); + }; + clearStream.on('error', function (err) { + console.error('Delete post stream error: ' + err.message); + }); + + var read = fs.createReadStream(path); + var parse = js.parse('*'); + + console.time('Time: '); + + var pipeline = read.pipe(parse).pipe(clearStream); + + pipeline.on('finish', function () { + console.timeEnd('Time: '); + }); +} + +function get (group, find, author, startPage, endPage) { + var pages; + if (startPage && endPage) { + pages = []; + for (var i = startPage; i <= endPage; i++) { + pages.push(i); + } + } + var wall = new ProgressBar('Getting wall [:bar] :current/:total = :percent :etas remaining ', {total: 10000, clear: true}); + + var total = 0; + var first, last; + var low, high; + + var processStream = new stream.Transform({ + objectMode: true + }); + processStream._transform = function (chunk, encoding, done) { + if (startPage ? chunk.page === startPage : (!first || chunk.page < low)) { + first = chunk.posts[0]; + low = chunk.page; + } else if (endPage ? chunk.page === endPage : (!last || chunk.page > high)) { + last = chunk.posts[chunk.posts.length - 1]; + high = chunk.page; + } + var response = processPage(group, chunk, author, find); + total += response.indices.length; + done(null, response); + chunk = null; + response = null; + }; + processStream.on('error', function (err) { + console.error('Stream processing error: ' + err.message); + }); + + var path = './roblox-js-wall.' + crypto.randomBytes(20).toString('hex') + '.temp'; + mainPath = path; + var write = fs.createWriteStream(path); + var stringify = js.stringify('[\n', ',\n', '\n]\n'); + var pipeline = processStream.pipe(stringify).pipe(write); + var promise = rbx.getWall({ + group: group, + page: pages, + view: true, + stream: processStream + }); var ivl = setInterval(function () { - deletion.update(thread.getStatus() / 100); + wall.update(promise.getStatus() / 100); }, 1000); - thread.then(function () { + promise.then(function () { clearInterval(ivl); - console.timeEnd('Time: '); + }) + .catch(function (err) { + console.error('Get wall post failed: ' + err.message); + }); + return new Promise(function (resolve, reject) { + pipeline.on('finish', function () { + resolve({ + path: path, + total: total, + first: first, + last: last + }); + }); }); } function init (group, username, password, find, author, startPage, endPage) { rbx.login(username, password) .then(function () { - var pages; - if (startPage && endPage) { - pages = []; - for (var i = startPage; i <= endPage; i++) { - pages.push(i); - } + return get(group, find, author, startPage, endPage); + }) + .then(function (response) { + if (response.total === 0) { + console.log('There are no wall posts to delete!'); + return; } - var wall = new ProgressBar('Getting wall [:bar] :current/:total = :percent :etas remaining ', {total: 10000, clear: true}); - var promise = rbx.getWall({ - group: group, - page: pages, - view: true - }); - promise.then(function (wall) { - var posts = wall.posts; - // Remember these are reversed, it starts off with all the posts on the wall and you are REMOVING the ones you DON'T want to delete from the array - for (var i = posts.length - 1; i >= 0; i--) { - var post = posts[i]; - if (author && post.author.name !== author) { // Delete all posts by Bob - posts.splice(i, 1); - } else if (find && post.content.includes(find)) { // Delete all posts that contain "Bob" - posts.splice(i, 1); - } - } - if (posts.length === 0) { - console.log('There are no messages to delete!'); + console.log('You are about to delete ' + response.total + ' wall posts selected from ' + (startPage && endPage ? ('page ' + startPage + ' to ' + endPage) : ('ALL pages'))); + console.log('The list starts from the post "' + response.first.content.substring(20) + '..." and ends with the post "' + response.last.content.substring(20) + '..."'); + prompt.get({ + name: 'yesno', + message: 'Are you sure you want to do this? y/n', + validator: /^y|n$/, + required: true, + warning: 'You must respond with "y" or "n"' + }, function (err, result) { + if (err) { + console.error('Prompt error: ' + err.message); return; } - console.log('You are about to delete ' + posts.length + ' messages selected from ' + (startPage && endPage ? ('page ' + startPage + ' to ' + endPage) : ('ALL pages'))); - console.log('The list starts from the message starting with "' + posts[0].content.substring(0, 20) + '..." and ends with the message starting with "' + posts[posts.length - 1].content.substring(0, 20) + '..."'); - prompt.get({ - name: 'yesno', - message: 'Are you sure you want to do this? y/n', - validator: /^y|n$/, - required: true, - warning: 'You must respond with "y" or "n"' - }, function (err, result) { - if (err) { - console.error('Prompt error: ' + err.message); - return; - } - if (result.yesno === 'y') { - clear(group, wall); - } else { - console.log('Aborted'); - process.exit(); - } - }); - }); - var ivl = setInterval(function () { - wall.update(promise.getStatus() / 100); - }, 1000); - promise.then(function () { - clearInterval(ivl); + if (result.yesno === 'y') { + clear(group, response.path, response.total); + } else { + console.log('Aborted'); + process.exit(); + } }); }); } @@ -138,3 +232,16 @@ prompt.get(schema, function (err, result) { } init(result.group, result.username, result.password, result.find, result.author, result.startPage, result.endPage); }); + +function shutdown (err) { + if (err && err.message) { + console.error('Fatal error: ' + err.message); + } + if (mainPath) { + clean(mainPath); + } +} + +process.on('SIGTERM', shutdown); +process.on('SIGINT', shutdown); +process.on('exit', shutdown); From cab4305705d65404ddc1f0528ca991db498f4a1e Mon Sep 17 00:00:00 2001 From: Joshua Lanese Date: Wed, 6 Sep 2017 23:33:38 -0700 Subject: [PATCH 3/6] Make sure error exists Without this shortPoll will crash whenever an error does not exist. --- lib/util/shortPoll.js | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/lib/util/shortPoll.js b/lib/util/shortPoll.js index 6516f39..711d163 100644 --- a/lib/util/shortPoll.js +++ b/lib/util/shortPoll.js @@ -55,7 +55,9 @@ exports.func = function (args) { if (stop) { return; } - evt.emit('error', err); + if (err) { + evt.emit('error', err); + } retries++; if (retries > max) { evt.emit('close', new Error('Max retries reached')); From 6db6729f7d132e7e9d59a9af7d49094e4085a4bd Mon Sep 17 00:00:00 2001 From: Joshua Lanese Date: Wed, 6 Sep 2017 23:34:16 -0700 Subject: [PATCH 4/6] Reject with error Forgot to create error for the message --- lib/group/getWallPost.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/group/getWallPost.js b/lib/group/getWallPost.js index 1df5e8d..399e50a 100644 --- a/lib/group/getWallPost.js +++ b/lib/group/getWallPost.js @@ -30,7 +30,7 @@ function findPost (jar, group, id, page, view, resolve, reject, min, max) { max = page - 1; } if (min > wall.totalPages || max <= 0) { - reject('Couldn\'t find post'); + reject(new Error('Couldn\'t find post')); return; } findPost(jar, group, id, Math.floor((min + max) / 2), view, resolve, reject, min, max); From 8de489240ef9ec9258957c1b9dd69db01771d229 Mon Sep 17 00:00:00 2001 From: Joshua Lanese Date: Wed, 6 Sep 2017 23:35:19 -0700 Subject: [PATCH 5/6] Fix After the forum merge subforum 46 was deleted and the function would no longer fetch the verification tokens correctly to make a post. --- lib/forum/forumPost.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/forum/forumPost.js b/lib/forum/forumPost.js index 39c65c7..a6b7244 100644 --- a/lib/forum/forumPost.js +++ b/lib/forum/forumPost.js @@ -60,7 +60,7 @@ exports.func = function (args) { url: '//forum.roblox.com/Forum/AddPost.aspx' + (args.forumId ? ('?ForumID=' + args.forumId) : ('?PostID=' + args.postId)), events: events, http: { - url: '//forum.roblox.com/Forum/AddPost.aspx?ForumID=46' // If you get the verification token from the replying URL that token will not work with a new thread. The other way around, however, it works for both. + url: '//forum.roblox.com/Forum/AddPost.aspx?ForumID=65' // If you get the verification token from the replying URL that token will not work with a new thread. The other way around, however, it works for both. } }) .then(function (result) { From b3e1cdae30cd0bc6089c6c356df8a625010762c5 Mon Sep 17 00:00:00 2001 From: Joshua Lanese Date: Thu, 19 Oct 2017 18:04:39 -0700 Subject: [PATCH 6/6] Patch --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 55f1543..b627b34 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "roblox-js", - "version": "4.0.1", + "version": "4.0.2", "description": "A node module that provides an interface for performing actions on ROBLOX, mostly for use with their HttpService feature.", "main": "lib/index.js", "scripts": {