diff --git a/api/analyze.js b/api/analyze.js index 1b97b25c..68c54994 100644 --- a/api/analyze.js +++ b/api/analyze.js @@ -1,3 +1,4 @@ +"use strict"; const zlib = require('zlib') const blocks = require('../misc/analysis/blocks.json') const colorStuff = require('../misc/analysis/colorProperties.json') @@ -7,121 +8,130 @@ const ids = require('../misc/analysis/objects.json') module.exports = async (app, req, res, level) => { - if (!level) { - level = { - name: (req.body.name || "Unnamed").slice(0, 64), - data: (req.body.data || "") - } + level ||= { + name: (req.body.name || "Unnamed").slice(0, 64), + data: (req.body.data || "") } let unencrypted = level.data.startsWith('kS') // some gdps'es don't encrypt level data let levelString = unencrypted ? level.data : Buffer.from(level.data, 'base64') if (unencrypted) { - const raw_data = level.data; - - const response_data = analyze_level(level, raw_data); - return res.send(response_data); - } else { + const raw_data = level.data + const response_data = analyze_level(level, raw_data) + return res.send(response_data) + } + else { zlib.unzip(levelString, (err, buffer) => { - if (err) { return res.status(500).send("-2"); } + if (err) return res.status(500).send("-2") - const raw_data = buffer.toString(); - const response_data = analyze_level(level, raw_data); - return res.send(response_data); - }); + const raw_data = buffer.toString() + const response_data = analyze_level(level, raw_data) + return res.send(response_data) + }) } } -function sortObj(obj, sortBy) { - var sorted = {} - var keys = !sortBy ? Object.keys(obj).sort((a,b) => obj[b] - obj[a]) : Object.keys(obj).sort((a,b) => obj[b][sortBy] - obj[a][sortBy]) +/** + * Sorts any `Object` by its keys + * @param {{}} obj + * @param {PropertyKey} [sortBy] optional inner key to sort + */ +const sortObj = (obj, sortBy) => { + let keys = Object.keys(obj) + .sort((a,b) => sortBy ? obj[b][sortBy] - obj[a][sortBy] : obj[b] - obj[a]) + let sorted = {} keys.forEach(x => {sorted[x] = obj[x]}) return sorted } -function parse_obj(obj, splitter, name_arr, valid_only) { - const s_obj = obj.split(splitter); - let robtop_obj = {}; +/** + * game-object (**not** JS `Object`) parser + * @param {string} obj + * @param {string} splitter + * @param {string[]} name_arr + * @param {boolean} [valid_only] + */ +const parse_obj = (obj, splitter, name_arr, valid_only) => { + const s_obj = obj.split(splitter) + let robtop_obj = {} // semi-useless optimization depending on where at node js you're at for (let i = 0, obj_l = s_obj.length; i < obj_l; i += 2) { - let k_name = s_obj[i]; + let k_name = s_obj[i] if (s_obj[i] in name_arr) { - if (!valid_only) { - k_name = name_arr[s_obj[i]]; - } - robtop_obj[k_name] = s_obj[i + 1]; + if (!valid_only) k_name = name_arr[s_obj[i]] + robtop_obj[k_name] = s_obj[i + 1] } } - return robtop_obj; + return robtop_obj } +/** + * @param {{}} level + * @param {string} rawData + */ function analyze_level(level, rawData) { let response = {}; - let blockCounts = {} - let miscCounts = {} - let triggerGroups = [] + let blockCounts = {}; + let miscCounts = {}; + /**@type {string[]}*/ + let triggerGroups = []; let highDetail = 0 - let alphaTriggers = [] + /**@type {{}[]}*/ + let alphaTriggers = []; let misc_objects = {}; let block_ids = {}; for (const [name, object_ids] of Object.entries(ids.misc)) { - const copied_ids = object_ids.slice(1); + const copied_ids = object_ids.slice(1) // funny enough, shift effects the original id list - copied_ids.forEach((object_id) => { - misc_objects[object_id] = name; - }); + copied_ids.forEach(object_id => { misc_objects[object_id] = name }) } - for (const [name, object_ids] of Object.entries(blocks)) { - object_ids.forEach((object_id) => { - block_ids[object_id] = name; - }); - } + for (const [name, object_ids] of Object.entries(blocks)) + object_ids.forEach(object_id => { block_ids[object_id] = name }); - const data = rawData.split(";"); - const header = data.shift(); + /**@type {(string|{})[]}*/ + const data = rawData.split(";") + const header = data.shift() let level_portals = []; let level_coins = []; let level_text = []; + // "why are these Objects instead of Arrays?" @Rudxain let orb_array = {}; let trigger_array = {}; - let last = 0; + let last = 0 - const obj_length = data.length; + const obj_length = data.length for (let i = 0; i < obj_length; ++i) { - obj = parse_obj(data[i], ',', properties); + let obj = parse_obj(data[i], ',', properties) - let id = obj.id + let {id} = obj if (id in ids.portals) { - obj.portal = ids.portals[id]; - level_portals.push(obj); + obj.portal = ids.portals[id] + level_portals.push(obj) } else if (id in ids.coins) { - obj.coin = ids.coins[id]; - level_coins.push(obj); + obj.coin = ids.coins[id] + level_coins.push(obj) } else if (id in ids.orbs) { - obj.orb = ids.orbs[id]; + obj.orb = ids.orbs[id] - if (obj.orb in orb_array) { - orb_array[obj.orb]++; - } else { - orb_array[obj.orb] = 1; - } + const orb = orb_array[obj.orb] + orb_array[obj.orb] = orb ? +orb + 1 : 1 } else if (id in ids.triggers) { - obj.trigger = ids.triggers[id]; + obj.trigger = ids.triggers[id] if (obj.trigger in trigger_array) { - trigger_array[obj.trigger]++; + trigger_array[obj.trigger]++ } else { - trigger_array[obj.trigger] = 1; + trigger_array[obj.trigger] = 1 } } @@ -130,60 +140,66 @@ function analyze_level(level, rawData) { } if (obj.triggerGroups) obj.triggerGroups.split('.').forEach(x => triggerGroups.push(x)) - if (obj.highDetail == 1) highDetail += 1 + if (obj.highDetail == 1) highDetail++ if (id in misc_objects) { - const name = misc_objects[id]; + const name = misc_objects[id] if (name in miscCounts) { - miscCounts[name][0] += 1; + miscCounts[name][0]++ } else { - miscCounts[name] = [1, ids.misc[name][0]]; + miscCounts[name] = [1, ids.misc[name][0]] } } if (id in block_ids) { - const name = block_ids[id]; + const name = block_ids[id] if (name in blockCounts) { - blockCounts[name] += 1; + blockCounts[name]++ } else { - blockCounts[name] = 1; + blockCounts[name] = 1 } } - if (obj.x) { // sometimes the field doesn't exist - last = Math.max(last, obj.x); - } + // sometimes the field doesn't exist + if (obj.x) last = Math.max(last, obj.x) - if (obj.trigger == "Alpha") { // invisible triggers - alphaTriggers.push(obj) - } + // invisible triggers + if (obj.trigger == "Alpha") alphaTriggers.push(obj) - data[i] = obj; + data[i] = obj } let invisTriggers = [] alphaTriggers.forEach(tr => { - if (tr.x < 500 && !tr.touchTriggered && !tr.spawnTriggered && tr.opacity == 0 && tr.duration == 0 - && alphaTriggers.filter(x => x.targetGroupID == tr.targetGroupID).length == 1) invisTriggers.push(Number(tr.targetGroupID)) + if ( + tr.x < 500 + && !tr.touchTriggered && !tr.spawnTriggered + && tr.opacity == 0 && tr.duration == 0 + && alphaTriggers.filter(x => x.targetGroupID == tr.targetGroupID).length == 1 + ) + invisTriggers.push(Number(tr.targetGroupID)) }) - response.level = { - name: level.name, id: level.id, author: level.author, playerID: level.playerID, accountID: level.accountID, large: level.large - } + response.level = {}; + ['name', 'id', 'author', 'playerID', 'accountID', 'large'].forEach(k => {response.level[k] = level[k]}) response.objects = data.length - 2 response.highDetail = highDetail response.settings = {} - response.portals = level_portals.sort(function (a, b) {return parseInt(a.x) - parseInt(b.x)}).map(x => x.portal + " " + Math.floor(x.x / (Math.max(last, 529.0) + 340.0) * 100) + "%").join(", ") - response.coins = level_coins.sort(function (a, b) {return parseInt(a.x) - parseInt(b.x)}).map(x => Math.floor(x.x / (Math.max(last, 529.0) + 340.0) * 100)) + // "I have no idea what to name this lmao" @Rudxain + let WTF = x => Math.floor(x.x / (Math.max(last, 529) + 340) * 100) + response.portals = level_portals.sort((a, b) => parseInt(a.x) - parseInt(b.x)).map(x => x.portal + " " + WTF(x) + "%").join(", ") + response.coins = level_coins.sort((a, b) => parseInt(a.x) - parseInt(b.x)).map(WTF) response.coinsVerified = level.verifiedCoins + /**@param {number[]} arr*/ + const sum = arr => arr.reduce((a, x) => a + x, 0) response.orbs = orb_array - response.orbs.total = Object.values(orb_array).reduce((a, x) => a + x, 0); // we already have an array of objects, use it + response.orbs.total = sum(Object.values(orb_array)) // we already have an array of objects, use it response.triggers = trigger_array - response.triggers.total = Object.values(trigger_array).reduce((a, x) => a + x, 0); + response.triggers.total = sum(Object.values(trigger_array)) response.triggerGroups = {} response.blocks = sortObj(blockCounts) @@ -191,7 +207,7 @@ function analyze_level(level, rawData) { response.colors = [] triggerGroups.forEach(x => { - if (response.triggerGroups['Group ' + x]) response.triggerGroups['Group ' + x] += 1 + if (response.triggerGroups['Group ' + x]) response.triggerGroups['Group ' + x]++ else response.triggerGroups['Group ' + x] = 1 }) @@ -202,99 +218,113 @@ function analyze_level(level, rawData) { // find alpha group with the most objects response.invisibleGroup = triggerKeys.find(x => invisTriggers.includes(x)) - response.text = level_text.sort(function (a, b) {return parseInt(a.x) - parseInt(b.x)}).map(x => [Buffer.from(x.message, 'base64').toString(), Math.round(x.x / last * 99) + "%"]) + response.text = level_text.sort((a, b) => parseInt(a.x) - parseInt(b.x)).map(x => [Buffer.from(x.message, 'base64').toString(), Math.round(x.x / last * 99) + "%"]) - const header_response = parse_header(header); - response.settings = header_response.settings; - response.colors = header_response.colors; + const header_response = parse_header(header) + response.settings = header_response.settings + response.colors = header_response.colors response.dataLength = rawData.length response.data = rawData - return response; + return response } -function parse_header(header) { - let response = {}; - response.settings = {}; - response.colors = []; +function parse_header(/**@type {string}*/ header) { + let response = {} + response.settings = {} + response.colors = [] - const header_keyed = parse_obj(header, ',', init.values, true); + const header_keyed = parse_obj(header, ',', init.values, true) - Object.keys(header_keyed).forEach(x => { - let val = init.values[x] + Object.keys(header_keyed).forEach(k => { + let val = init.values[k] + /**@type {string}*/ let name = val[0] - let property = header_keyed[x] + let property = header_keyed[k] switch (val[1]) { case 'list': val = init[(val[0] + "s")][property]; - break; + break case 'number': val = Number(property); - break; + break case 'bump': val = Number(property) + 1; - break; + break case 'bool': val = property != "0"; - break; + break case 'extra-legacy-color': { // scope? // you can only imagine my fear when i discovered this was a thing // these literally are keys set the value, and to convert this to the color list we have to do this fun messy thing that shouldn't exist // since i wrote the 1.9 color before this, a lot of explaination will be there instead - const colorInfo = name.split('-'); - const color = colorInfo[2]; // r,g,b - const channel = colorInfo[1]; + const colorInfo = name.split('-') + /** r,g,b */ + const color = colorInfo[2] + const channel = colorInfo[1] + + // first we create the color object + if (color == 'r') response.colors.push({"channel": channel, "opacity": 1}) - if (color == 'r') { - // first we create the color object - response.colors.push({"channel": channel, "opacity": 1}); - } // from here we touch the color object - let currentChannel = response.colors.find(k => k.channel == channel); - if (color == 'blend') { - currentChannel.blending = true; // only one color has blending though lol - } else if (color == 'pcol' && property != 0) { - currentChannel.pColor = property; - } - currentChannel[color] = property; - break; + let currentChannel = response.colors.find(k => k.channel == channel) + if (color == 'blend') currentChannel.blending = true // only one color has blending though lol + if (color == 'pcol' && property != 0) currentChannel.pColor = property + + currentChannel[color] = property + break } case 'legacy-color': { // if a level has a legacy color, we can assume that it does not have a kS38 at all - const color = parse_obj(property, "_", colorStuff.properties); + const color = parse_obj(property, "_", colorStuff.properties) let colorObj = color // so here we parse the color to something understandable by the rest // slightly smart naming but it is also pretty gross // in a sense - the name would be something like legacy-G -> G - const colorVal = name.split('-').pop() + const colorVal = name.split('-').at(-1) colorObj.channel = colorVal // from here stuff can continue as normal, ish - if (colorObj.pColor == "-1" || colorObj.pColor == "0") delete colorObj.pColor; - colorObj.opacity = 1; // 1.9 colors don't have this! - if (colorObj.blending && colorObj.blending == '1') colorObj.blending = true; // 1.9 colors manage to always think they're blending - they're not - else delete colorObj.blending; - - if (colorVal == '3DL') { response.colors.splice(4, 0, colorObj); } // hardcode the position of 3DL, it typically goes at the end due to how RobTop make the headers - else if (colorVal == 'Line') { colorObj.blending = true; response.colors.push(colorObj); } // in line with 2.1 behavior - else { response.colors.push(colorObj); } // bruh whatever was done to make the color list originally was long - break; + if (colorObj.pColor == "-1" || colorObj.pColor == "0") + delete colorObj.pColor + colorObj.opacity = 1 // 1.9 colors don't have this! + + if (colorObj?.blending === '1') + colorObj.blending = true // 1.9 colors manage to always think they're blending - they're not + else + delete colorObj.blending + + switch (colorVal) { + case '3DL': + response.colors.splice(4, 0, colorObj) // hardcode the position of 3DL, it typically goes at the end due to how RobTop make the headers + break + + case 'Line': { + colorObj.blending = true; response.colors.push(colorObj) // in line with 2.1 behavior + break + } + + default: + response.colors.push(colorObj) // bruh whatever was done to make the color list originally was long + break + } + break } case 'colors': { let colorList = property.split("|") colorList.forEach((x, y) => { const color = parse_obj(x, "_", colorStuff.properties) let colorObj = color - if (!color.channel) return colorList = colorList.filter((h, i) => y != i) + if (!color.channel) return colorList = colorList.filter((_, i) => y != i) if (colorStuff.channels[colorObj.channel]) colorObj.channel = colorStuff.channels[colorObj.channel] - if (colorObj.channel > 1000) return; + if (colorObj.channel > 1000) return if (colorStuff.channels[colorObj.copiedChannel]) colorObj.copiedChannel = colorStuff.channels[colorObj.copiedChannel] - if (colorObj.copiedChannel > 1000) delete colorObj.copiedChannel; + if (colorObj.copiedChannel > 1000) delete colorObj.copiedChannel if (colorObj.pColor == "-1") delete colorObj.pColor if (colorObj.blending) colorObj.blending = true if (colorObj.copiedHSV) { @@ -303,37 +333,39 @@ function parse_header(header) { hsv.forEach((x, y) => { colorObj.copiedHSV[colorStuff.hsv[y]] = x }) colorObj.copiedHSV['s-checked'] = colorObj.copiedHSV['s-checked'] == 1 colorObj.copiedHSV['v-checked'] = colorObj.copiedHSV['v-checked'] == 1 - if (colorObj.copyOpacity == 1) colorObj.copyOpacity = true + if (colorObj.copyOpacity == 1) colorObj.copyOpacity = true } colorObj.opacity = +Number(colorObj.opacity).toFixed(2) colorList[y] = colorObj - }); + }) // we assume this is only going to be run once so... some stuff can go here colorList = colorList.filter(x => typeof x == "object") if (!colorList.find(x => x.channel == "Obj")) colorList.push({"r": "255", "g": "255", "b": "255", "channel": "Obj", "opacity": "1"}) const specialSort = ["BG", "G", "G2", "Line", "Obj", "3DL"] - let specialColors = colorList.filter(x => isNaN(x.channel)).sort(function (a, b) {return specialSort.indexOf( a.channel ) > specialSort.indexOf( b.channel ) } ) - let regularColors = colorList.filter(x => !isNaN(x.channel)).sort(function(a, b) {return (+a.channel) - (+b.channel) } ); + let specialColors = colorList.filter(x => isNaN(x.channel)).sort((a, b) => specialSort.indexOf(a.channel) > specialSort.indexOf(b.channel)) + let regularColors = colorList.filter(x => !isNaN(x.channel)).sort((a, b) => a.channel - b.channel) response.colors = specialColors.concat(regularColors) - break; + break } } response.settings[name] = val }) - if (!response.settings.ground || response.settings.ground > 17) response.settings.ground = 1 - if (!response.settings.background || response.settings.background > 20) response.settings.background = 1 - if (!response.settings.font) response.settings.font = 1 + if (!response.settings.ground || response.settings.ground > 17) + response.settings.ground = 1 + if (!response.settings.background || response.settings.background > 20) + response.settings.background = 1 + if (!response.settings.font) + response.settings.font = 1 - if (response.settings.alternateLine == 2) response.settings.alternateLine = true - else response.settings.alternateLine = false + response.settings.alternateLine = response.settings.alternateLine == 2 Object.keys(response.settings).filter(k => { // this should be parsed into color list instead - if (k.includes('legacy')) delete response.settings[k]; - }); + if (k.includes('legacy')) delete response.settings[k] + }) - delete response.settings['colors']; - return response; + delete response.settings['colors'] + return response } diff --git a/api/comments.js b/api/comments.js index f48f6e10..9148764d 100644 --- a/api/comments.js +++ b/api/comments.js @@ -1,3 +1,4 @@ +"use strict"; const Player = require('../classes/Player.js') module.exports = async (app, req, res) => { @@ -8,8 +9,8 @@ module.exports = async (app, req, res) => { if (count > 1000) count = 1000 let params = { - userID : req.params.id, - accountID : req.params.id, + userID : req.params.id, + accountID : req.params.id, levelID: req.params.id, page: +req.query.page || 0, count, @@ -20,7 +21,7 @@ module.exports = async (app, req, res) => { if (req.query.type == "commentHistory") { path = "getGJCommentHistory"; delete params.levelID } else if (req.query.type == "profile") path = "getGJAccountComments20" - req.gdRequest(path, req.gdParams(params), function(err, resp, body) { + req.gdRequest(path, req.gdParams(params), function(err, resp, body) { if (err) return res.sendError() @@ -32,7 +33,7 @@ module.exports = async (app, req, res) => { if (!comments.length) return res.status(204).send([]) let pages = body.split('#')[1].split(":") - let lastPage = +Math.ceil(+pages[0] / +pages[2]); + let lastPage = +Math.ceil(+pages[0] / +pages[2]) let commentArray = [] @@ -41,7 +42,7 @@ module.exports = async (app, req, res) => { var x = c[0] //comment info var y = c[1] //account info - if (!x[2]) return; + if (!x[2]) return let comment = {} comment.content = Buffer.from(x[2], 'base64').toString(); @@ -50,9 +51,9 @@ module.exports = async (app, req, res) => { comment.date = (x[9] || "?") + req.timestampSuffix if (comment.content.endsWith("⍟") || comment.content.endsWith("☆")) { comment.content = comment.content.slice(0, -1) - comment.browserColor = true + comment.browserColor = true } - + if (req.query.type != "profile") { let commentUser = new Player(y) Object.keys(commentUser).forEach(k => { @@ -74,7 +75,7 @@ module.exports = async (app, req, res) => { commentArray.push(comment) - }) + }) return res.send(commentArray) diff --git a/api/download.js b/api/download.js index 7d4cc12d..2cdb899e 100644 --- a/api/download.js +++ b/api/download.js @@ -1,3 +1,4 @@ +"use strict"; const request = require('request') const fs = require('fs') const Level = require('../classes/Level.js') @@ -5,27 +6,27 @@ const Level = require('../classes/Level.js') module.exports = async (app, req, res, api, ID, analyze) => { function rejectLevel() { - if (!api) return res.redirect('search/' + req.params.id) - else return res.sendError() + return !api ? res.redirect('search/' + req.params.id) : res.sendError() } if (req.offline) { - if (!api && levelID < 0) return res.redirect('/') - return rejectLevel() + return !api && levelID < 0 ? res.redirect('/') : rejectLevel() } let levelID = ID || req.params.id - if (levelID == "daily") levelID = -1 - else if (levelID == "weekly") levelID = -2 - else levelID = levelID.replace(/[^0-9]/g, "") + levelID = ( + levelID == "daily" ? -1 : + levelID == "weekly" ? -2 : + levelID.replace(/\D/g, "") + ) req.gdRequest('downloadGJLevel22', { levelID }, function (err, resp, body) { - if (err) { - if (analyze && api && req.server.downloadsDisabled) return res.status(403).send("-3") - else if (!api && levelID < 0) return res.redirect(`/?daily=${levelID * -1}`) - else return rejectLevel() - } + if (err) return ( + analyze && api && req.server.downloadsDisabled ? res.status(403).send("-3") + : !api && levelID < 0 ? res.redirect(`/?daily=${levelID * -1}`) + : rejectLevel() + ) let authorData = body.split("#")[3] // daily/weekly only, most likely @@ -43,7 +44,7 @@ module.exports = async (app, req, res, api, ID, analyze) => { if (err2 && (foundID || authorData)) { let authorInfo = foundID || authorData.split(":") level.author = authorInfo[1] || "-" - level.accountID = authorInfo[0] && authorInfo[0].includes(",") ? "0" : authorInfo[0] + level.accountID = authorInfo[0]?.includes(",") ? "0" : authorInfo[0] } else if (!err && b2 != '-1') { @@ -72,7 +73,7 @@ module.exports = async (app, req, res, api, ID, analyze) => { if (api) return res.send(level) else return fs.readFile('./html/level.html', 'utf8', function (err, data) { - let html = data; + let html = data let variables = Object.keys(level) variables.forEach(x => { let regex = new RegExp(`\\[\\[${x.toUpperCase()}\\]\\]`, "g") @@ -89,7 +90,7 @@ module.exports = async (app, req, res, api, ID, analyze) => { level.nextDaily = +dailyTime level.nextDailyTimestamp = Math.round((Date.now() + (+dailyTime * 1000)) / 100000) * 100000 return sendLevel() - }) + }) } else if (req.server.demonList && level.difficulty == "Extreme Demon") { diff --git a/api/gauntlets.js b/api/gauntlets.js index d0661f31..efc1368a 100644 --- a/api/gauntlets.js +++ b/api/gauntlets.js @@ -1,3 +1,4 @@ +"use strict"; let cache = {} let gauntletNames = ["Fire", "Ice", "Poison", "Shadow", "Lava", "Bonus", "Chaos", "Demon", "Time", "Crystal", "Magic", "Spike", "Monster", "Doom", "Death"] @@ -6,17 +7,19 @@ module.exports = async (app, req, res) => { if (req.offline) return res.sendError() let cached = cache[req.id] - if (app.config.cacheGauntlets && cached && cached.data && cached.indexed + 2000000 > Date.now()) return res.send(cached.data) // half hour cache + if (app.config.cacheGauntlets && cached && cached.data && cached.indexed + 2000000 > Date.now()) + return res.send(cached.data) // half hour cache req.gdRequest('getGJGauntlets21', {}, function (err, resp, body) { if (err) return res.sendError() - let gauntlets = body.split('#')[0].split('|').map(x => app.parseResponse(x)).filter(x => x[3]) + let gauntlets = body.split('#', 1)[0].split('|').map(x => app.parseResponse(x)).filter(x => x[3]) let gauntletList = gauntlets.map(x => ({ id: +x[1], name: gauntletNames[+x[1] - 1] || "Unknown", levels: x[3].split(",") })) - if (app.config.cacheGauntlets) cache[req.id] = {data: gauntletList, indexed: Date.now()} + if (app.config.cacheGauntlets) + cache[req.id] = {data: gauntletList, indexed: Date.now()} res.send(gauntletList) }) - + } \ No newline at end of file diff --git a/api/leaderboards/accurate.js b/api/leaderboards/accurate.js index cfea414a..3c15a6ae 100644 --- a/api/leaderboards/accurate.js +++ b/api/leaderboards/accurate.js @@ -1,5 +1,6 @@ -const {GoogleSpreadsheet} = require('google-spreadsheet'); -const sheet = new GoogleSpreadsheet('1ADIJvAkL0XHGBDhO7PP9aQOuK3mPIKB2cVPbshuBBHc'); // accurate leaderboard spreadsheet +"use strict"; +const {GoogleSpreadsheet} = require('google-spreadsheet') +const sheet = new GoogleSpreadsheet('1ADIJvAkL0XHGBDhO7PP9aQOuK3mPIKB2cVPbshuBBHc') // accurate leaderboard spreadsheet let indexes = ["stars", "coins", "demons", "diamonds"] @@ -9,39 +10,41 @@ let caches = [{"stars": null, "coins": null, "demons": null, "diamonds": null}, module.exports = async (app, req, res, post) => { - // Accurate leaderboard returns 418 because private servers do not use. - if (req.isGDPS) return res.status(418).send("-2") - if (!app.sheetsKey) return res.status(500).send([]) - let gdMode = post || req.query.hasOwnProperty("gd") - let modMode = !gdMode && req.query.hasOwnProperty("mod") - let cache = caches[gdMode ? 2 : modMode ? 1 : 0] - - let type = req.query.type ? req.query.type.toLowerCase() : 'stars' - if (type == "usercoins") type = "coins" - if (!indexes.includes(type)) type = "stars" - if (lastIndex[modMode ? 1 : 0][type] + 600000 > Date.now() && cache[type]) return res.send(gdMode ? cache[type] : JSON.parse(cache[type])) // 10 min cache - - sheet.useApiKey(app.sheetsKey) - sheet.loadInfo().then(async () => { - let tab = sheet.sheetsById[1555821000] - await tab.loadCells('A2:H2') - - let cellIndex = indexes.findIndex(x => type == x) - if (modMode) cellIndex += indexes.length - - let cell = tab.getCell(1, cellIndex).value - if (!cell || typeof cell != "string" || cell.startsWith("GoogleSpreadsheetFormulaError")) { console.log("Spreadsheet Error:"); console.log(cell); return res.sendError() } - let leaderboard = JSON.parse(cell.replace(/~( |$)/g, "")) - - let gdFormatting = "" - leaderboard.forEach(x => { - app.userCache(req.id, x.accountID, x.playerID, x.username) - gdFormatting += `1:${x.username}:2:${x.playerID}:13:${x.coins}:17:${x.usercoins}:6:${x.rank}:9:${x.icon.icon}:10:${x.icon.col1}:11:${x.icon.col2}:14:${forms.indexOf(x.icon.form)}:15:${x.icon.glow ? 2 : 0}:16:${x.accountID}:3:${x.stars}:8:${x.cp}:46:${x.diamonds}:4:${x.demons}|` - }) - caches[modMode ? 1 : 0][type] = JSON.stringify(leaderboard) - caches[2][type] = gdFormatting - lastIndex[modMode ? 1 : 0][type] = Date.now() - return res.send(gdMode ? gdFormatting : leaderboard) - + // Accurate leaderboard returns 418 because private servers do not use. + if (req.isGDPS) return res.status(418).send("-2") + if (!app.sheetsKey) return res.status(500).send([]) + const gdMode = post || req.query.hasOwnProperty("gd") + const modMode = !gdMode && req.query.hasOwnProperty("mod") + let cache = caches[gdMode ? 2 : modMode ? 1 : 0] + + let type = req.query.type ? req.query.type.toLowerCase() : 'stars' + if (type == "usercoins") type = "coins" + if (!indexes.includes(type)) type = "stars" + if (lastIndex[modMode ? 1 : 0][type] + 600000 > Date.now() && cache[type]) + return res.send(gdMode ? cache[type] : JSON.parse(cache[type])) // 10 min cache + + sheet.useApiKey(app.sheetsKey) + sheet.loadInfo().then(async () => { + let tab = sheet.sheetsById[1555821000] + await tab.loadCells('A2:H2') + + let cellIndex = indexes.indexOf(type) + if (modMode) cellIndex += indexes.length + + let cell = tab.getCell(1, cellIndex).value + if (!cell || typeof cell != "string" || cell.startsWith("GoogleSpreadsheetFormulaError")) { + console.log("Spreadsheet Error:"); console.log(cell); return res.sendError() + } + let leaderboard = JSON.parse(cell.replace(/~( |$)/g, "")) + + let gdFormatting = "" + leaderboard.forEach(x => { + app.userCache(req.id, x.accountID, x.playerID, x.username) + gdFormatting += `1:${x.username}:2:${x.playerID}:13:${x.coins}:17:${x.usercoins}:6:${x.rank}:9:${x.icon.icon}:10:${x.icon.col1}:11:${x.icon.col2}:14:${forms.indexOf(x.icon.form)}:15:${x.icon.glow ? 2 : 0}:16:${x.accountID}:3:${x.stars}:8:${x.cp}:46:${x.diamonds}:4:${x.demons}|` + }) + caches[modMode ? 1 : 0][type] = JSON.stringify(leaderboard) + caches[2][type] = gdFormatting + lastIndex[modMode ? 1 : 0][type] = Date.now() + return res.send(gdMode ? gdFormatting : leaderboard) }) } \ No newline at end of file diff --git a/api/leaderboards/boomlings.js b/api/leaderboards/boomlings.js index cb0e1822..27096574 100644 --- a/api/leaderboards/boomlings.js +++ b/api/leaderboards/boomlings.js @@ -1,12 +1,15 @@ +"use strict"; const request = require('request') module.exports = async (app, req, res) => { - // Accurate leaderboard returns 418 because Private servers do not use. - if (req.isGDPS) return res.status(418).send("0") + // Accurate leaderboard returns 418 because Private servers do not use. + if (req.isGDPS) return res.status(418).send("0") - request.post('http://robtopgames.com/Boomlings/get_scores.php', { - form : { secret: app.config.params.secret || "Wmfd2893gb7", name: "Player" } }, function(err, resp, body) { + request.post( + 'http://robtopgames.com/Boomlings/get_scores.php', + { form : { secret: app.config.params.secret || "Wmfd2893gb7", name: "Player" } }, + function(err, resp, body) { if (err || !body || body == 0) return res.status(500).send("0") @@ -24,7 +27,7 @@ module.exports = async (app, req, res) => { score: +scores.slice(3, 10), boomling: +visuals.slice(5, 7), boomlingLevel: +visuals.slice(2, 4), - powerups: [+visuals.slice(7, 9), +visuals.slice(9, 11), +visuals.slice(11, 13)].map(x => (x > 8 || x < 1) ? 0 : x), + powerups: [+visuals.slice(7, 9), +visuals.slice(9, 11), +visuals.slice(11, 13)].map(x => (x > 8 || x < 1) ? 0 : x), unknownVisual: +visuals.slice(0, 2), unknownScore: +scores.slice(0, 1), @@ -34,10 +37,10 @@ module.exports = async (app, req, res) => { if (!user.boomling || user.boomling > 66 || user.boomling < 0) user.boomling = 0 if (!user.boomlingLevel || user.boomlingLevel > 25 || user.boomlingLevel < 1) user.boomlingLevel = 25 - users.push(user) + users.push(user) }) return res.send(users) - - }) + } + ) } \ No newline at end of file diff --git a/api/leaderboards/leaderboardLevel.js b/api/leaderboards/leaderboardLevel.js index 86e74804..7ce4c5f4 100644 --- a/api/leaderboards/leaderboardLevel.js +++ b/api/leaderboards/leaderboardLevel.js @@ -1,53 +1,50 @@ -const colors = require('../../iconkit/sacredtexts/colors.json'); +"use strict"; +const colors = require('../../iconkit/sacredtexts/colors.json') module.exports = async (app, req, res) => { if (req.offline) return res.sendError() - let amount = 100; - let count = req.query.count ? parseInt(req.query.count) : null - if (count && count > 0) { - if (count > 200) amount = 200 - else amount = count; - } - - let params = { - levelID: req.params.id, - accountID: app.id, - gjp: app.gjp, - type: req.query.hasOwnProperty("week") ? "2" : "1", - } - - req.gdRequest('getGJLevelScores211', params, function(err, resp, body) { - - if (err) return res.status(500).send({error: true, lastWorked: app.timeSince(req.id)}) - scores = body.split('|').map(x => app.parseResponse(x)).filter(x => x[1]) - if (!scores.length) return res.status(500).send([]) - else app.trackSuccess(req.id) - - scores.forEach(x => { - let keys = Object.keys(x) - x.rank = x[6] - x.username = x[1] - x.percent = +x[3] - x.coins = +x[13] - x.playerID = x[2] - x.accountID = x[16] - x.date = x[42] + req.timestampSuffix - x.icon = { - form: ['icon', 'ship', 'ball', 'ufo', 'wave', 'robot', 'spider'][+x[14]], - icon: +x[9], - col1: +x[10], - col2: +x[11], - glow: +x[15] > 1, - col1RGB: colors[x[10]] || colors["0"], - col2RGB: colors[x[11]] || colors["3"] - } - keys.forEach(k => delete x[k]) - app.userCache(req.id, x.accountID, x.playerID, x.username) - }) - - return res.send(scores.slice(0, amount)) - - }) + let amount = 100 + let count = req.query.count ? parseInt(req.query.count) : null + if (count && count > 0) amount = Math.min(count, 200) + + let params = { + levelID: req.params.id, + accountID: app.id, + gjp: app.gjp, + type: req.query.hasOwnProperty("week") ? "2" : "1", + } + + req.gdRequest('getGJLevelScores211', params, function(err, resp, body) { + + if (err) return res.status(500).send({error: true, lastWorked: app.timeSince(req.id)}) + scores = body.split('|').map(x => app.parseResponse(x)).filter(x => x[1]) + if (!scores.length) return res.status(500).send([]) + app.trackSuccess(req.id) + + scores.forEach(x => { + let keys = Object.keys(x) + x.rank = x[6] + x.username = x[1] + x.percent = +x[3] + x.coins = +x[13] + x.playerID = x[2] + x.accountID = x[16] + x.date = x[42] + req.timestampSuffix + x.icon = { + form: ['icon', 'ship', 'ball', 'ufo', 'wave', 'robot', 'spider'][+x[14]], + icon: +x[9], + col1: +x[10], + col2: +x[11], + glow: +x[15] > 1, + col1RGB: colors[x[10]] || colors["0"], + col2RGB: colors[x[11]] || colors["3"] + } + keys.forEach(k => delete x[k]) + app.userCache(req.id, x.accountID, x.playerID, x.username) + }) + + return res.send(scores.slice(0, amount)) + }) } \ No newline at end of file diff --git a/api/leaderboards/scores.js b/api/leaderboards/scores.js index 3a5baa38..68bf8a11 100644 --- a/api/leaderboards/scores.js +++ b/api/leaderboards/scores.js @@ -1,33 +1,35 @@ +"use strict"; const Player = require('../../classes/Player.js') module.exports = async (app, req, res) => { - if (req.offline) return res.sendError() + if (req.offline) return res.sendError() - let amount = 100; - let count = req.query.count ? parseInt(req.query.count) : null - if (count && count > 0) { - if (count > 10000) amount = 10000 - else amount = count; - } + let amount = 100 + let count = req.query.count ? parseInt(req.query.count) : 0 + if (count > 0) amount = Math.min(count, 10000) - let params = {count: amount, type: "top"} + let params = {count: amount, type: "top"} - if (["creators", "creator", "cp"].some(x => req.query.hasOwnProperty(x) || req.query.type == x)) params.type = "creators" - else if (["week", "weekly"].some(x => req.query.hasOwnProperty(x) || req.query.type == x)) params.type = "week" - else if (["global", "relative"].some(x => req.query.hasOwnProperty(x) || req.query.type == x)) { - params.type = "relative" - params.accountID = req.query.accountID - } + let isInQuery = (...args) => args.some(x => req.query.hasOwnProperty(x) || req.query.type == x) - req.gdRequest('getGJScores20', params, function(err, resp, body) { + if (isInQuery("creators", "creator", "cp")) + params.type = "creators" + else if (isInQuery("week", "weekly")) + params.type = "week" + else if (isInQuery("global", "relative")) { + params.type = "relative" + params.accountID = req.query.accountID + } - if (err) return res.sendError() - scores = body.split('|').map(x => app.parseResponse(x)).filter(x => x[1]) - if (!scores.length) return res.sendError() + req.gdRequest('getGJScores20', params, function(err, resp, body) { - scores = scores.map(x => new Player(x)) - scores.forEach(x => app.userCache(req.id, x.accountID, x.playerID, x.username)) - return res.send(scores.slice(0, amount)) - }) + if (err) return res.sendError() + scores = body.split('|').map(x => app.parseResponse(x)).filter(x => x[1]) + if (!scores.length) return res.sendError() + + scores = scores.map(x => new Player(x)) + scores.forEach(x => app.userCache(req.id, x.accountID, x.playerID, x.username)) + return res.send(scores.slice(0, amount)) + }) } \ No newline at end of file diff --git a/api/level.js b/api/level.js index b5ea7ebe..887e8ed1 100644 --- a/api/level.js +++ b/api/level.js @@ -1,3 +1,4 @@ +"use strict"; const request = require('request') const fs = require('fs') const Level = require('../classes/Level.js') @@ -5,17 +6,14 @@ const Level = require('../classes/Level.js') module.exports = async (app, req, res, api, analyze) => { function rejectLevel() { - if (!api) return res.redirect('search/' + req.params.id) - else return res.sendError() + return !api ? res.redirect('search/' + req.params.id) : res.sendError() } if (req.offline) return rejectLevel() let levelID = req.params.id - if (levelID == "daily") return app.run.download(app, req, res, api, 'daily', analyze) - else if (levelID == "weekly") return app.run.download(app, req, res, api, 'weekly', analyze) - else if (levelID.match(/[^0-9]/)) return rejectLevel() - else levelID = levelID.replace(/[^0-9]/g, "") + if (levelID == "daily" || levelID == "weekly") return app.run.download(app, req, res, api, levelID, analyze) + else if (/\D/.test(levelID)) return rejectLevel() if (analyze || req.query.hasOwnProperty("download")) return app.run.download(app, req, res, api, levelID, analyze) @@ -23,9 +21,11 @@ module.exports = async (app, req, res, api, analyze) => { if (err || body.startsWith("##")) return rejectLevel() - let preRes = body.split('#')[0].split('|', 10) - let author = body.split('#')[1].split('|')[0].split(':') - let song = '~' + body.split('#')[2]; + // "revolutionary name XD" @Rudxain + const bodySplit = body.split('#') + let preRes = bodySplit[0].split('|', 10) + let author = bodySplit[1].split('|', 1)[0].split(':') + let song = '~' + bodySplit[2] song = app.parseResponse(song, '~|~') let levelInfo = app.parseResponse(preRes.find(x => x.startsWith(`1:${levelID}`)) || preRes[0]) @@ -40,7 +40,7 @@ module.exports = async (app, req, res, api, analyze) => { if (api) return res.send(level) else return fs.readFile('./html/level.html', 'utf8', function (err, data) { - let html = data; + let html = data let filteredSong = level.songName.replace(/[^ -~]/g, "") // strip off unsupported characters level.songName = filteredSong || level.songName let variables = Object.keys(level) diff --git a/api/mappacks.js b/api/mappacks.js index 5b4a6d3e..eb5006c2 100644 --- a/api/mappacks.js +++ b/api/mappacks.js @@ -1,12 +1,14 @@ +"use strict"; let difficulties = ["auto", "easy", "normal", "hard", "harder", "insane", "demon", "demon-easy", "demon-medium", "demon-insane", "demon-extreme"] let cache = {} module.exports = async (app, req, res) => { if (req.offline) return res.sendError() - + let cached = cache[req.id] - if (app.config.cacheMapPacks && cached && cached.data && cached.indexed + 5000000 > Date.now()) return res.send(cached.data) // 1.5 hour cache + if (app.config.cacheMapPacks && cached && cached.data && cached.indexed + 5000000 > Date.now()) + return res.send(cached.data) // 1.5 hour cache let params = { count: 250, page: 0 } let packs = [] @@ -15,7 +17,7 @@ module.exports = async (app, req, res) => { if (err) return res.sendError() - let newPacks = body.split('#')[0].split('|').map(x => app.parseResponse(x)).filter(x => x[2]) + let newPacks = body.split('#', 1)[0].split('|').map(x => app.parseResponse(x)).filter(x => x[2]) packs = packs.concat(newPacks) // not all GDPS'es support the count param, which means recursion time!!! @@ -23,7 +25,7 @@ module.exports = async (app, req, res) => { params.page++ return mapPackLoop() } - + let mappacks = packs.map(x => ({ // "packs.map()" laugh now please id: +x[1], name: x[2], diff --git a/api/messages/countMessages.js b/api/messages/countMessages.js index 717457b1..8aea16ea 100644 --- a/api/messages/countMessages.js +++ b/api/messages/countMessages.js @@ -1,9 +1,11 @@ +"use strict"; module.exports = async (app, req, res) => { + const send = (msg, c=400) => res.status(c).send(msg) - if (req.method !== 'POST') return res.status(405).send("Method not allowed.") + if (req.method !== 'POST') return send("Method not allowed.", 405) - if (!req.body.accountID) return res.status(400).send("No account ID provided!") - if (!req.body.password) return res.status(400).send("No password provided!") + if (!req.body.accountID) return send("No account ID provided!") + if (!req.body.password) return send("No password provided!") let params = { accountID: req.body.accountID, @@ -13,11 +15,11 @@ module.exports = async (app, req, res) => { req.gdRequest('getGJUserInfo20', params, function (err, resp, body) { - if (err) return res.status(400).send(`Error counting messages! Messages get blocked a lot so try again later, or make sure your username and password are entered correctly. Last worked: ${app.timeSince(req.id)} ago.`) - else app.trackSuccess(req.id) + if (err) return send(`Error counting messages! Messages get blocked a lot so try again later, or make sure your username and password are entered correctly. Last worked: ${app.timeSince(req.id)} ago.`) + app.trackSuccess(req.id) let count = app.parseResponse(body)[38] - if (!count) return res.status(400).send("Error fetching unread messages!") - else res.send(count) + if (!count) return send("Error fetching unread messages!") + res.send(count) }) } \ No newline at end of file diff --git a/api/messages/deleteMessage.js b/api/messages/deleteMessage.js index 8b6cc26f..727d01fd 100644 --- a/api/messages/deleteMessage.js +++ b/api/messages/deleteMessage.js @@ -1,23 +1,26 @@ +"use strict"; module.exports = async (app, req, res) => { + const send = (msg, c=400) => res.status(c).send(msg) - if (req.method !== 'POST') return res.status(405).send("Method not allowed.") + if (req.method !== 'POST') return send("Method not allowed.", 405) - if (!req.body.accountID) return res.status(400).send("No account ID provided!") - if (!req.body.password) return res.status(400).send("No password provided!") - if (!req.body.id) return res.status(400).send("No message ID(s) provided!") + if (!req.body.accountID) return send("No account ID provided!") + if (!req.body.password) return send("No password provided!") + if (!req.body.id) return send("No message ID(s) provided!") let params = { accountID: req.body.accountID, gjp: app.xor.encrypt(req.body.password, 37526), + // serialize to CSV if needed messages: Array.isArray(req.body.id) ? req.body.id.map(x => x.trim()).join(",") : req.body.id, } - let deleted = params.messages.split(",").length + let deleted = params.messages.split(",").length // CSV record count req.gdRequest('deleteGJMessages20', params, function (err, resp, body) { - if (body != 1) return res.status(400).send(`The Geometry Dash servers refused to delete the message! Try again later, or make sure your username and password are entered correctly. Last worked: ${app.timeSince(req.id)} ago.`) - else res.send(`${deleted == 1 ? "1 message" : `${deleted} messages`} deleted!`) + if (body != 1) return send(`The Geometry Dash servers refused to delete the message! Try again later, or make sure your username and password are entered correctly. Last worked: ${app.timeSince(req.id)} ago.`) + res.send(`${deleted} message${deleted == 1 ? "" : "s"} deleted!`) app.trackSuccess(req.id) }) diff --git a/api/messages/fetchMessage.js b/api/messages/fetchMessage.js index edb871ea..e3b53fe0 100644 --- a/api/messages/fetchMessage.js +++ b/api/messages/fetchMessage.js @@ -1,9 +1,11 @@ +"use strict"; module.exports = async (app, req, res) => { + const send = (msg, c=400) => res.status(c).send(msg) - if (req.method !== 'POST') return res.status(405).send("Method not allowed.") + if (req.method !== 'POST') return send("Method not allowed.", 405) - if (!req.body.accountID) return res.status(400).send("No account ID provided!") - if (!req.body.password) return res.status(400).send("No password provided!") + if (!req.body.accountID) return send("No account ID provided!") + if (!req.body.password) return send("No password provided!") let params = req.gdParams({ accountID: req.body.accountID, @@ -13,24 +15,25 @@ module.exports = async (app, req, res) => { req.gdRequest('downloadGJMessage20', params, function (err, resp, body) { - if (err) return res.status(400).send(`Error fetching message! Try again later, or make sure your username and password are entered correctly. Last worked: ${app.timeSince(req.id)} ago.`) - else app.trackSuccess(req.id) + if (err) return send(`Error fetching message! Try again later, or make sure your username and password are entered correctly. Last worked: ${app.timeSince(req.id)} ago.`) + app.trackSuccess(req.id) let x = app.parseResponse(body) - let msg = {} - msg.id = x[1]; - msg.playerID = x[3] - msg.accountID = x[2] - msg.author = x[6] - msg.subject = Buffer.from(x[4], "base64").toString().replace(/^Re: ☆/, "Re: ") - msg.content = app.xor.decrypt(x[5], 14251) - msg.date = x[7] + req.timestampSuffix - if (msg.subject.endsWith("☆") || msg.subject.startsWith("☆")) { - if (msg.subject.endsWith("☆")) msg.subject = msg.subject.slice(0, -1) - else msg.subject = msg.subject.slice(1) - msg.browserColor = true - } - + let subject = Buffer.from(x[4], "base64").toString().replace(/^Re: ☆/, "Re: ") + let msg = { + id: x[1], + playerID: x[3], + accountID: x[2], + author: x[6], + subject, + content: app.xor.decrypt(x[5], 14251), + date: x[7] + req.timestampSuffix + } + if (/^☆|☆$/.test(subject)) { + msg.subject = subject.slice(...(subject.endsWith("☆") ? [0, -1] : [1])) + msg.browserColor = true + } + return res.send(msg) }) diff --git a/api/messages/getMessages.js b/api/messages/getMessages.js index 40da8a39..e0c18a1c 100644 --- a/api/messages/getMessages.js +++ b/api/messages/getMessages.js @@ -1,10 +1,12 @@ +"use strict"; module.exports = async (app, req, res) => { + const send = (msg, c=400) => res.status(c).send(msg) - if (req.method !== 'POST') return res.status(405).send("Method not allowed.") + if (req.method !== 'POST') return send("Method not allowed.", 405) if (req.body.count) return app.run.countMessages(app, req, res) - if (!req.body.accountID) return res.status(400).send("No account ID provided!") - if (!req.body.password) return res.status(400).send("No password provided!") + if (!req.body.accountID) return send("No account ID provided!") + if (!req.body.password) return send("No password provided!") let params = req.gdParams({ accountID: req.body.accountID, @@ -15,26 +17,26 @@ module.exports = async (app, req, res) => { req.gdRequest('getGJMessages20', params, function (err, resp, body) { - if (err) return res.status(400).send(`Error fetching messages! Messages get blocked a lot so try again later, or make sure your username and password are entered correctly. Last worked: ${app.timeSince(req.id)} ago.`) + if (err) return send(`Error fetching messages! Messages get blocked a lot so try again later, or make sure your username and password are entered correctly. Last worked: ${app.timeSince(req.id)} ago.`) else app.trackSuccess(req.id) let messages = body.split("|").map(msg => app.parseResponse(msg)) let messageArray = [] messages.forEach(x => { - let msg = {} - - msg.id = x[1]; - msg.playerID = x[3] - msg.accountID = x[2] - msg.author = x[6] - msg.subject = Buffer.from(x[4], "base64").toString().replace(/^Re: ☆/, "Re: ") - msg.date = x[7] + req.timestampSuffix - msg.unread = x[8] != "1" - if (msg.subject.endsWith("☆") || msg.subject.startsWith("☆")) { - if (msg.subject.endsWith("☆")) msg.subject = msg.subject.slice(0, -1) - else msg.subject = msg.subject.slice(1) - msg.browserColor = true - } + let subject = Buffer.from(x[4], "base64").toString().replace(/^Re: ☆/, "Re: ") + let msg = { + id: x[1], + playerID: x[3], + accountID: x[2], + author: x[6], + subject, + date: x[7] + req.timestampSuffix, + unread: x[8] != "1" + } + if (/^☆|☆$/.test(subject)) { + msg.subject = subject.slice(...(subject.endsWith("☆") ? [0, -1] : [1])) + msg.browserColor = true + } app.userCache(req.id, msg.accountID, msg.playerID, msg.author) messageArray.push(msg) diff --git a/api/messages/sendMessage.js b/api/messages/sendMessage.js index 36bb9c65..463448ec 100644 --- a/api/messages/sendMessage.js +++ b/api/messages/sendMessage.js @@ -1,25 +1,33 @@ +"use strict"; module.exports = async (app, req, res) => { + const send = (msg, c=400) => res.status(c).send(msg) - if (req.method !== 'POST') return res.status(405).send("Method not allowed.") + if (req.method !== 'POST') return send("Method not allowed.", 405) - if (!req.body.targetID) return res.status(400).send("No target ID provided!") - if (!req.body.message) return res.status(400).send("No message provided!") - if (!req.body.accountID) return res.status(400).send("No account ID provided!") - if (!req.body.password) return res.status(400).send("No password provided!") + if (!req.body.targetID) return send("No target ID provided!") + if (!req.body.message) return send("No message provided!") + if (!req.body.accountID) return send("No account ID provided!") + if (!req.body.password) return send("No password provided!") - let subject = Buffer.from(req.body.subject ? (req.body.color ? "☆" : "") + (req.body.subject.slice(0, 50)) : (req.body.color ? "☆" : "") + "No subject").toString('base64').replace(/\//g, '_').replace(/\+/g, "-") + let subject = Buffer.from( + (req.body.color ? "☆" : "") + (req.body.subject ? req.body.subject.slice(0, 50) : "No subject") + ).toString('base64url') let body = app.xor.encrypt(req.body.message.slice(0, 300), 14251) let params = req.gdParams({ accountID: req.body.accountID, gjp: app.xor.encrypt(req.body.password, 37526), toAccountID: req.body.targetID, - subject, body, + subject, body }) req.gdRequest('uploadGJMessage20', params, function (err, resp, body) { - if (body != 1) return res.status(400).send(`The Geometry Dash servers refused to send the message! Try again later, or make sure your username and password are entered correctly. Last worked: ${app.timeSince(req.id)} ago.`) - else res.send('Message sent!') + if (body != 1) return send( + `The Geometry Dash servers refused to send the message! `+ + `Try again later, or make sure your username and password are entered correctly. `+ + `Last worked: ${app.timeSince(req.id)} ago.` + ) + res.send('Message sent!') app.trackSuccess(req.id) }) diff --git a/api/post/like.js b/api/post/like.js index 0997238b..86a61266 100644 --- a/api/post/like.js +++ b/api/post/like.js @@ -1,17 +1,31 @@ +"use strict"; const crypto = require('crypto') -function sha1(data) { return crypto.createHash("sha1").update(data, "binary").digest("hex"); } +const sha1 = data => crypto.createHash("sha1").update(data, "binary").digest("hex") module.exports = async (app, req, res) => { + const send = (msg, c=400) => res.status(c).send(msg) - if (req.method !== 'POST') return res.status(405).send("Method not allowed.") + if (req.method !== 'POST') return send("Method not allowed.", 405) + + if (!req.body.ID) return send("No ID provided!") + if (!req.body.accountID) return send("No account ID provided!") + if (!req.body.password) return send("No password provided!") + if (!req.body.like) return send("No like flag provided! (1=like, 0=dislike)") + if (!req.body.type) return send("No type provided! (1=level, 2=comment, 3=profile") + if (!req.body.extraID) return send("No extra ID provided! (this should be a level ID, account ID, or '0' for levels") + /* + // A compound error message is more helpful, but IDK if this may cause bugs, + // so this is commented-out + let errMsg = "" + if (!req.body.ID) errMsg += "No ID provided!\n" + if (!req.body.accountID) errMsg += "No account ID provided!\n" + if (!req.body.password) errMsg += "No password provided!\n" + if (!req.body.like) errMsg += "No like flag provided! (1=like, 0=dislike)\n" + if (!req.body.type) errMsg += "No type provided! (1=level, 2=comment, 3=profile\n" + if (!req.body.extraID) errMsg += "No extra ID provided! (this should be a level ID, account ID, or '0' for levels)\n" + if (errMsg) return send(errMsg) + */ - if (!req.body.ID) return res.status(400).send("No ID provided!") - if (!req.body.accountID) return res.status(400).send("No account ID provided!") - if (!req.body.password) return res.status(400).send("No password provided!") - if (!req.body.like) return res.status(400).send("No like flag provided! (1=like, 0=dislike)") - if (!req.body.type) return res.status(400).send("No type provided! (1=level, 2=comment, 3=profile") - if (!req.body.extraID) return res.status(400).send("No extra ID provided! (this should be a level ID, account ID, or '0' for levels") - let params = { udid: '0', uuid: '0', @@ -25,14 +39,15 @@ module.exports = async (app, req, res) => { params.special = req.body.extraID.toString() params.type = req.body.type.toString() - let chk = params.special + params.itemID + params.like + params.type + params.rs + params.accountID + params.udid + params.uuid + "ysg6pUrtjn0J" + let chk = ""; + ['special', 'itemID', 'like', 'type', 'rs', 'accountID', 'udid', 'uuid'].forEach(k => chk += params[k]) + chk += "ysg6pUrtjn0J" chk = sha1(chk) chk = app.xor.encrypt(chk, 58281) - params.chk = chk req.gdRequest('likeGJItem211', params, function (err, resp, body) { - if (err) return res.status(400).send(`The Geometry Dash servers rejected your vote! Try again later, or make sure your username and password are entered correctly. Last worked: ${app.timeSince(req.id)} ago.`) + if (err) return send(`The Geometry Dash servers rejected your vote! Try again later, or make sure your username and password are entered correctly. Last worked: ${app.timeSince(req.id)} ago.`) else app.trackSuccess(req.id) res.send((params.like == 1 ? 'Successfully liked!' : 'Successfully disliked!') + " (this will only take effect if this is your first time doing so)") }) diff --git a/api/post/postComment.js b/api/post/postComment.js index 5027efc2..8b792c88 100644 --- a/api/post/postComment.js +++ b/api/post/postComment.js @@ -1,31 +1,46 @@ +"use strict"; const crypto = require('crypto') -function sha1(data) { return crypto.createHash("sha1").update(data, "binary").digest("hex"); } +const sha1 = data => crypto.createHash("sha1").update(data, "binary").digest("hex") -let rateLimit = {}; +let rateLimit = {} let cooldown = 15000 // GD has a secret rate limit and doesn't return -1 when a comment is rejected, so this keeps track +// converts timestamp miliseconds to s (wrapped-around minutes) function getTime(time) { - let seconds = Math.ceil(time / 1000); - seconds = seconds % 60; - return seconds} + let seconds = Math.ceil(time / 1000) + seconds %= 60 + return seconds +} module.exports = async (app, req, res) => { + const send = (msg, c=400) => res.status(c).send(msg) - if (req.method !== 'POST') return res.status(405).send("Method not allowed.") + if (req.method !== 'POST') return send("Method not allowed.", 405) - if (!req.body.comment) return res.status(400).send("No comment provided!") - if (!req.body.username) return res.status(400).send("No username provided!") - if (!req.body.levelID) return res.status(400).send("No level ID provided!") - if (!req.body.accountID) return res.status(400).send("No account ID provided!") - if (!req.body.password) return res.status(400).send("No password provided!") + if (!req.body.comment) return send("No comment provided!") + if (!req.body.username) return send("No username provided!") + if (!req.body.levelID) return send("No level ID provided!") + if (!req.body.accountID) return send("No account ID provided!") + if (!req.body.password) return send("No password provided!") + /* + // A compound error message is more helpful, but IDK if this may cause bugs, + // so this is commented-out + let errMsg = "" + if (!req.body.comment) errMsg += "No comment provided!\n" + if (!req.body.username) errMsg += "No username provided!\n" + if (!req.body.levelID) errMsg += "No level ID provided!\n" + if (!req.body.accountID) errMsg += "No account ID provided!\n" + if (!req.body.password) errMsg += "No password provided!\n" + if (errMsg) return send(errMsg) + */ - if (req.body.comment.includes('\n')) return res.status(400).send("Comments cannot contain line breaks!") + if (req.body.comment.includes('\n')) return send("Comments cannot contain line breaks!") + + if (rateLimit[req.body.username]) return send(`Please wait ${getTime(rateLimit[req.body.username] + cooldown - Date.now())} seconds before posting another comment!`) - if (rateLimit[req.body.username]) return res.status(400).send(`Please wait ${getTime(rateLimit[req.body.username] + cooldown - Date.now())} seconds before posting another comment!`) - let params = { percent: 0 } - params.comment = Buffer.from(req.body.comment + (req.body.color ? "☆" : "")).toString('base64').replace(/\//g, '_').replace(/\+/g, "-") + params.comment = Buffer.from(req.body.comment + (req.body.color ? "☆" : "")).toString('base64url') params.gjp = app.xor.encrypt(req.body.password, 37526) params.levelID = req.body.levelID.toString() params.accountID = req.body.accountID.toString() @@ -40,15 +55,22 @@ module.exports = async (app, req, res) => { params.chk = chk req.gdRequest('uploadGJComment21', params, function (err, resp, body) { - if (err) return res.status(400).send(`The Geometry Dash servers rejected your comment! Try again later, or make sure your username and password are entered correctly. Last worked: ${app.timeSince(req.id)} ago.`) + if (err) return send( + `The Geometry Dash servers rejected your comment! `+ + `Try again later, or make sure your username and password are entered correctly. `+ + `Last worked: ${app.timeSince(req.id)} ago.` + ) if (body.startsWith("temp")) { let banStuff = body.split("_") - return res.status(400).send(`You have been banned from commenting for ${(parseInt(banStuff[1]) / 86400).toFixed(0)} days. Reason: ${banStuff[2] || "None"}`) + return send( + `You have been banned from commenting for ${(parseInt(banStuff[1]) / 86400).toFixed(0)} days. `+ + `Reason: ${banStuff[2] || "None"}` + ) } res.send(`Comment posted to level ${params.levelID} with ID ${body}`) app.trackSuccess(req.id) - rateLimit[req.body.username] = Date.now(); - setTimeout(() => {delete rateLimit[req.body.username]; }, cooldown); + rateLimit[req.body.username] = Date.now() + setTimeout(() => {delete rateLimit[req.body.username]}, cooldown); }) } \ No newline at end of file diff --git a/api/post/postProfileComment.js b/api/post/postProfileComment.js index b9d0ae3a..3eeda13f 100644 --- a/api/post/postProfileComment.js +++ b/api/post/postProfileComment.js @@ -1,20 +1,32 @@ +"use strict"; const crypto = require('crypto') -function sha1(data) { return crypto.createHash("sha1").update(data, "binary").digest("hex"); } +const sha1 = data => crypto.createHash("sha1").update(data, "binary").digest("hex") module.exports = async (app, req, res) => { + const send = (msg, c=400) => res.status(c).send(msg) - if (req.method !== 'POST') return res.status(405).send("Method not allowed.") + if (req.method !== 'POST') return send("Method not allowed.", 405) - if (!req.body.comment) return res.status(400).send("No comment provided!") - if (!req.body.username) return res.status(400).send("No username provided!") - if (!req.body.accountID) return res.status(400).send("No account ID provided!") - if (!req.body.password) return res.status(400).send("No password provided!") + if (!req.body.comment) return send("No comment provided!") + if (!req.body.username) return send("No username provided!") + if (!req.body.accountID) return send("No account ID provided!") + if (!req.body.password) return send("No password provided!") + /* + // A compound error message is more helpful, but IDK if this may cause bugs, + // so this is commented-out + let errMsg = "" + if (!req.body.comment) errMsg += "No comment provided!\n" + if (!req.body.username) errMsg += "No username provided!\n" + if (!req.body.accountID) errMsg += "No account ID provided!\n" + if (!req.body.password) errMsg += "No password provided!\n" + if (errMsg) return send(errMsg) + */ + + if (req.body.comment.includes('\n')) return send("Profile posts cannot contain line breaks!") - if (req.body.comment.includes('\n')) return res.status(400).send("Profile posts cannot contain line breaks!") - let params = { cType: '1' } - params.comment = Buffer.from(req.body.comment.slice(0, 190) + (req.body.color ? "☆" : "")).toString('base64').replace(/\//g, '_').replace(/\+/g, "-") + params.comment = Buffer.from(req.body.comment.slice(0, 190) + (req.body.color ? "☆" : "")).toString('base64url') params.gjp = app.xor.encrypt(req.body.password, 37526) params.accountID = req.body.accountID.toString() params.userName = req.body.username @@ -25,10 +37,10 @@ module.exports = async (app, req, res) => { params.chk = chk req.gdRequest('uploadGJAccComment20', params, function (err, resp, body) { - if (err) return res.status(400).send(`The Geometry Dash servers rejected your profile post! Try again later, or make sure your username and password are entered correctly. Try again later, or make sure your username and password are entered correctly. Last worked: ${app.timeSince(req.id)} ago.`) + if (err) return send(`The Geometry Dash servers rejected your profile post! Try again later, or make sure your username and password are entered correctly. Try again later, or make sure your username and password are entered correctly. Last worked: ${app.timeSince(req.id)} ago.`) else if (body.startsWith("temp")) { let banStuff = body.split("_") - return res.status(400).send(`You have been banned from commenting for ${(parseInt(banStuff[1]) / 86400).toFixed(0)} days. Reason: ${banStuff[2] || "None"}`) + return send(`You have been banned from commenting for ${(parseInt(banStuff[1]) / 86400).toFixed(0)} days. Reason: ${banStuff[2] || "None"}`) } else app.trackSuccess(req.id) res.send(`Comment posted to ${params.userName} with ID ${body}`) diff --git a/api/profile.js b/api/profile.js index 15a9587e..95a6813b 100644 --- a/api/profile.js +++ b/api/profile.js @@ -1,13 +1,15 @@ +"use strict"; const fs = require('fs') const Player = require('../classes/Player.js') module.exports = async (app, req, res, api, getLevels) => { - - if (req.offline) { - if (!api) return res.redirect('/search/' + req.params.id) - else return res.sendError() + function rejectLevel() { + // this variant has an extra "/" + return !api ? res.redirect('/search/' + req.params.id) : res.sendError() } - + + if (req.offline) return rejectLevel() + let username = getLevels || req.params.id let probablyID if (username.endsWith(".") && req.isGDPS) { @@ -17,18 +19,18 @@ module.exports = async (app, req, res, api, getLevels) => { let accountMode = !req.query.hasOwnProperty("player") && Number(req.params.id) let foundID = app.userCache(req.id, username) let skipRequest = accountMode || foundID || probablyID - let searchResult; + let searchResult // if you're searching by account id, an intentional error is caused to skip the first request to the gd servers. see i pulled a sneaky on ya. (fuck callbacks man) - req.gdRequest(skipRequest ? "" : 'getGJUsers20', skipRequest ? {} : { str: username, page: 0 }, function (err1, res1, b1) { + req.gdRequest(skipRequest ? "" : 'getGJUsers20', skipRequest ? {} : { str: username, page: 0 }, function (err1, res1, b1) { if (foundID) searchResult = foundID[0] else if (accountMode || err1 || b1 == '-1' || b1.startsWith("<") || !b1) searchResult = probablyID ? username : req.params.id - else if (!req.isGDPS) searchResult = app.parseResponse(b1.split("|")[0])[16] + else if (!req.isGDPS) searchResult = app.parseResponse(b1.split("|", 1)[0])[16] else { // GDPS's return multiple users, GD no longer does this let userResults = b1.split("|").map(x => app.parseResponse(x)) searchResult = userResults.find(x => x[1].toLowerCase() == username.toLowerCase() || x[2] == username) || "" - if (searchResult) searchResult = searchResult[16] + searchResult &&= searchResult[16] } if (getLevels) { @@ -40,20 +42,17 @@ module.exports = async (app, req, res, api, getLevels) => { let account = app.parseResponse(body || "") let dumbGDPSError = req.isGDPS && (!account[16] || account[1].toLowerCase() == "undefined") - - if (err2 || dumbGDPSError) { - if (!api) return res.redirect('/search/' + req.params.id) - else return res.sendError() - } - + + if (err2 || dumbGDPSError) return rejectLevel() + if (!foundID) app.userCache(req.id, account[16], account[2], account[1]) - - let userData = new Player(account) - + + let userData = new Player(account) + if (api) return res.send(userData) else fs.readFile('./html/profile.html', 'utf8', function(err, data) { - let html = data; + let html = data let variables = Object.keys(userData) variables.forEach(x => { let regex = new RegExp(`\\[\\[${x.toUpperCase()}\\]\\]`, "g") @@ -61,7 +60,6 @@ module.exports = async (app, req, res, api, getLevels) => { }) return res.send(html) }) - - }) }) - } \ No newline at end of file + }) +} \ No newline at end of file diff --git a/api/search.js b/api/search.js index 86e7ff37..0c728b1a 100644 --- a/api/search.js +++ b/api/search.js @@ -1,94 +1,92 @@ +"use strict"; const request = require('request') -const music = require('../misc/music.json') const Level = require('../classes/Level.js') let demonList = {} module.exports = async (app, req, res) => { + const {query} = req + if (req.offline) return res.status(500).send(query.hasOwnProperty("err") ? "err" : "-1") - if (req.offline) return res.status(500).send(req.query.hasOwnProperty("err") ? "err" : "-1") - - let demonMode = req.query.hasOwnProperty("demonlist") || req.query.hasOwnProperty("demonList") || req.query.type == "demonlist" || req.query.type == "demonList" + let demonMode = query.hasOwnProperty("demonlist") || query.hasOwnProperty("demonList") || query.type == "demonlist" || query.type == "demonList" if (demonMode) { if (!req.server.demonList) return res.sendError(400) let dList = demonList[req.id] if (!dList || !dList.list.length || dList.lastUpdated + 600000 < Date.now()) { // 10 minute cache - return request.get(req.server.demonList + 'api/v2/demons/listed/?limit=100', function (err1, resp1, list1) { + let demonStr = req.server.demonList + 'api/v2/demons/listed/?limit=100' + return request.get(demonStr, function (err1, resp1, list1) { if (err1) return res.sendError() - else return request.get(req.server.demonList + 'api/v2/demons/listed/?limit=100&after=100', function (err2, resp2, list2) { + else return request.get(demonStr + '&after=100', function (err2, resp2, list2) { if (err2) return res.sendError() - demonList[req.id] = {list: JSON.parse(list1).concat(JSON.parse(list2)).map(x => String(x.level_id)), lastUpdated: Date.now()} + demonList[req.id] = { + list: JSON.parse(list1).concat(JSON.parse(list2)) + .map(x => String(x.level_id)), + lastUpdated: Date.now() + } return app.run.search(app, req, res) }) }) } } - let amount = 10; - let count = req.isGDPS ? 10 : +req.query.count - if (count && count > 0) { - if (count > 500) amount = 500 - else amount = count; - } - + let amount = 10 + let count = req.isGDPS ? 10 : +query.count + if (count && count > 0) amount = Math.min(count, 500) + let filters = { str: req.params.text, - diff: req.query.diff, - demonFilter: req.query.demonFilter, - page: req.query.page || 0, - gauntlet: req.query.gauntlet || 0, - len: req.query.length, - song: req.query.songID, - followed: req.query.creators, - - featured: req.query.hasOwnProperty("featured") ? 1 : 0, - originalOnly: req.query.hasOwnProperty("original") ? 1 : 0, - twoPlayer: req.query.hasOwnProperty("twoPlayer") ? 1 : 0, - coins: req.query.hasOwnProperty("coins") ? 1 : 0, - epic: req.query.hasOwnProperty("epic") ? 1 : 0, - star: req.query.hasOwnProperty("starred") ? 1 : 0, - noStar: req.query.hasOwnProperty("noStar") ? 1 : 0, - customSong: req.query.hasOwnProperty("customSong") ? 1 : 0, - - type: req.query.type || 0, + diff: query.diff, + demonFilter: query.demonFilter, + page: query.page || 0, + gauntlet: query.gauntlet || 0, + len: query.length, + song: query.songID, + followed: query.creators, + + featured: query.hasOwnProperty("featured") ? 1 : 0, + originalOnly: query.hasOwnProperty("original") ? 1 : 0, + twoPlayer: query.hasOwnProperty("twoPlayer") ? 1 : 0, + coins: query.hasOwnProperty("coins") ? 1 : 0, + epic: query.hasOwnProperty("epic") ? 1 : 0, + star: query.hasOwnProperty("starred") ? 1 : 0, + noStar: query.hasOwnProperty("noStar") ? 1 : 0, + customSong: query.hasOwnProperty("customSong") ? 1 : 0, + + type: query.type || 0, count: amount } - if (req.query.type) { - let filterCheck = req.query.type.toLowerCase() - switch(filterCheck) { - case 'mostdownloaded': filters.type = 1; break; - case 'mostliked': filters.type = 2; break; - case 'trending': filters.type = 3; break; - case 'recent': filters.type = 4; break; - case 'featured': filters.type = 6; break; - case 'magic': filters.type = 7; break; - case 'awarded': filters.type = 11; break; - case 'starred': filters.type = 11; break; - case 'halloffame': filters.type = 16; break; - case 'hof': filters.type = 16; break; - case 'gdw': filters.type = 17; break; - case 'gdworld': filters.type = 17; break; + if (query.type) { + let filterCheck = query.type.toLowerCase() + let typeMap = { + 'mostdownloaded': 1, 'mostliked': 2, + 'trending': 3, 'recent': 4, + 'featured': 6, 'magic': 7, + 'awarded': 11, 'starred': 11, + 'halloffame': 16, 'hof': 16, + 'gdw': 17, 'gdworld': 17 } + if (typeMap.hasOwnProperty(filterCheck)) // JIC there's no match + filters.type = typeMap[filterCheck] } - if (req.query.hasOwnProperty("user")) { + if (query.hasOwnProperty("user")) { let accountCheck = app.userCache(req.id, filters.str) filters.type = 5 if (accountCheck) filters.str = accountCheck[1] - else if (!filters.str.match(/^[0-9]*$/)) return app.run.profile(app, req, res, null, req.params.text) - } + else if ( !(/^\d*$/).test(filters.str) ) return app.run.profile(app, req, res, null, req.params.text) + } - if (req.query.hasOwnProperty("creators")) filters.type = 12 + if (query.hasOwnProperty("creators")) filters.type = 12 let listSize = 10 - if (demonMode || req.query.gauntlet || req.query.type == "saved" || ["mappack", "list", "saved"].some(x => req.query.hasOwnProperty(x))) { + if (demonMode || query.gauntlet || query.type == "saved" || ["mappack", "list", "saved"].some(x => query.hasOwnProperty(x))) { filters.type = 10 filters.str = demonMode ? demonList[req.id].list : filters.str.split(",") listSize = filters.str.length filters.str = filters.str.slice(filters.page*amount, filters.page*amount + amount) if (!filters.str.length) return res.sendError(400) - filters.str = filters.str.map(x => String(Number(x) + (+req.query.l || 0))).join() + filters.str = filters.str.map(x => String(Number(x) + (+query.l || 0))).join() filters.page = 0 } @@ -103,25 +101,25 @@ module.exports = async (app, req, res) => { let authorList = {} let songList = {} let authors = splitBody[1].split('|') - let songs = splitBody[2]; songs = songs.split('~:~').map(x => app.parseResponse(`~${x}~`, '~|~')) + let songs = splitBody[2].split('~:~').map(x => app.parseResponse(`~${x}~`, '~|~')) songs.forEach(x => {songList[x['~1']] = x['2']}) authors.forEach(x => { - if (x.startsWith('~')) return - let arr = x.split(':') - authorList[arr[0]] = [arr[1], arr[2]]}) + if (x.startsWith('~')) return + let arr = x.split(':') + authorList[arr[0]] = [arr[1], arr[2]] + }) let levelArray = preRes.map(x => app.parseResponse(x)).filter(x => x[1]) let parsedLevels = [] levelArray.forEach((x, y) => { - let songSearch = songs.find(y => y['~1'] == x[35]) || [] let level = new Level(x, req.server).getSongInfo(songSearch) if (!level.id) return - level.author = authorList[x[6]] ? authorList[x[6]][0] : "-"; - level.accountID = authorList[x[6]] ? authorList[x[6]][1] : "0"; + level.author = authorList[x[6]] ? authorList[x[6]][0] : "-" + level.accountID = authorList[x[6]] ? authorList[x[6]][1] : "0" if (demonMode) { if (!y) level.demonList = req.server.demonList @@ -133,21 +131,21 @@ module.exports = async (app, req, res) => { //this is broken if you're not on page 0, blame robtop if (filters.page == 0 && y == 0 && splitBody[3]) { - let pages = splitBody[3].split(":"); + let pages = splitBody[3].split(":") if (filters.gauntlet) { // gauntlet page stuff - level.results = levelArray.length + level.results = levelArray.length level.pages = 1 } else if (filters.type == 10) { // custom page stuff level.results = listSize - level.pages = +Math.ceil(listSize / (amount || 10)) + level.pages = Math.ceil(listSize / (amount || 10)) } else { // normal page stuff - level.results = +pages[0]; - level.pages = +pages[0] == 9999 ? 1000 : +Math.ceil(pages[0] / amount); + level.results = +pages[0] + level.pages = +pages[0] == 9999 ? 1000 : Math.ceil(pages[0] / amount) } } diff --git a/api/song.js b/api/song.js index 1439318d..efcf294f 100644 --- a/api/song.js +++ b/api/song.js @@ -1,10 +1,12 @@ +"use strict"; module.exports = async (app, req, res) => { if (req.offline) return res.sendError() let songID = req.params.song req.gdRequest('getGJSongInfo', {songID: songID}, function(err, resp, body) { - if (err) return res.sendError(400) - return res.send(!body.startsWith("-") && body.length > 10) + return err + ? res.sendError(400) + : res.send(!body.startsWith("-") && body.length > 10) }) -} \ No newline at end of file +} diff --git a/assets/css/boomlings.css b/assets/css/boomlings.css index 814fdd60..4aff6a06 100644 --- a/assets/css/boomlings.css +++ b/assets/css/boomlings.css @@ -184,7 +184,7 @@ h2 { .brownBox { border: 2.5vh solid transparent; border-radius: 3vh; - background-color: #995533; + background-color: #953; border-image: url('../../assets/brownbox.png') 10% round; } @@ -205,4 +205,4 @@ h2 { -webkit-transform: rotate(360deg); transform: rotate(360deg) } -} \ No newline at end of file +} diff --git a/assets/css/browser.css b/assets/css/browser.css index 1df0dac9..dc2cbf66 100644 --- a/assets/css/browser.css +++ b/assets/css/browser.css @@ -54,9 +54,9 @@ img, .noSelect { } .supercenter { - position: absolute; - top: 50%; - left: 50%; + position: absolute; + top: 50%; + left: 50%; transform: translate(-50%,-50%); } @@ -252,7 +252,7 @@ textarea::-webkit-scrollbar { width: 1.5vh; background: #6d3c24; } - + textarea::-webkit-scrollbar-thumb { background: rgb(83, 47, 28); overflow: hidden; @@ -345,7 +345,7 @@ input::-webkit-outer-spin-button, input::-webkit-inner-spin-button { border-width: 2.5vh; border-style: solid; border-radius: 3vh; - background-color: #995533; + background-color: #953; border-image: url('../../assets/brownbox.png') 10% round; } @@ -353,7 +353,7 @@ input::-webkit-outer-spin-button, input::-webkit-inner-spin-button { border-width: 2.5vh; border-style: solid; border-radius: 3vh; - background-color: #334499; + background-color: #349; border-image: url('../../assets/bluebox.png') 10% round; } @@ -563,8 +563,8 @@ input::-webkit-outer-spin-button, input::-webkit-inner-spin-button { } .popup { - position: fixed; - display: none; + position: fixed; + display: none; width: 100%; height: 100%; top: 0; left: 0; right: 0; bottom: 0; @@ -619,9 +619,9 @@ input::-webkit-outer-spin-button, input::-webkit-inner-spin-button { } #scoreTabs { - position: absolute; + position: absolute; text-align: center; - top: 2.45%; + top: 2.45%; width: 100%; margin-left: -13.5vh } @@ -629,13 +629,13 @@ input::-webkit-outer-spin-button, input::-webkit-inner-spin-button { #searchBox { background-color: rgb(173, 115, 76); width: 122vh; - height: 75%; - overflow-y: auto; + height: 75%; + overflow-y: auto; overflow-x: hidden; } -#searchBox::-webkit-scrollbar, #statusDiv::-webkit-scrollbar, #commentBox::-webkit-scrollbar { - display: none; +#searchBox::-webkit-scrollbar, #statusDiv::-webkit-scrollbar, #commentBox::-webkit-scrollbar { + display: none; } .searchResult { @@ -703,7 +703,7 @@ input::-webkit-outer-spin-button, input::-webkit-inner-spin-button { .comment h2, .smallGold { - background: -webkit-linear-gradient(#e28000, #ffee44); + background: -webkit-linear-gradient(#e28000, #fe4); background-clip: text; width: fit-content; vertical-align: top; @@ -902,7 +902,7 @@ input::-webkit-outer-spin-button, input::-webkit-inner-spin-button { background-color: #BE6F3F; overflow-y: scroll; scrollbar-width: none; - -ms-overflow-style: none; + -ms-overflow-style: none; } #msgList::-webkit-scrollbar { @@ -1029,7 +1029,7 @@ input::-webkit-outer-spin-button, input::-webkit-inner-spin-button { } .analysis::-webkit-scrollbar-thumb, .levelCode::-webkit-scrollbar-thumb { - background: rgba(0, 0, 0, 0.3); + background: rgba(0, 0, 0, 0.3); overflow: hidden; } @@ -1292,7 +1292,7 @@ input::-webkit-outer-spin-button, input::-webkit-inner-spin-button { .gdpslogo { margin-bottom: 0%; border-radius: 10%; - filter: drop-shadow(0.5vh 0.5vh 0.15vh rgba(0, 0, 0, 0.6)) + filter: drop-shadow(0.5vh 0.5vh 0.15vh rgba(0, 0, 0, 0.6)) } .menuDisabled, .downloadDisabled { @@ -1311,14 +1311,14 @@ input::-webkit-outer-spin-button, input::-webkit-inner-spin-button { cr { color: #ff5a5a } co { color: #ffa54b } -cy { color: #ffff00 } +cy { color: #ff0 } cg { color: #40e348 } -ca { color: #00ffff } +ca { color: #0ff } cb { color: #60abef } -cp { color: #ff00ff } +cp { color: #f0f } .red { - color: #ff0000 !important; + color: #f00 !important; } .yellow { @@ -1350,11 +1350,11 @@ cp { color: #ff00ff } } .brightblue { - color: #99ffff + color: #9ff } .brightred { - color: #ffaaaa; + color: #faa; } .gray { @@ -1380,10 +1380,10 @@ cp { color: #ff00ff } @-moz-keyframes spin { 100% { -moz-transform: rotate(360deg); } } @-webkit-keyframes spin { 100% { -webkit-transform: rotate(360deg); } } -@keyframes spin { - 100% { - -webkit-transform: rotate(360deg); - transform:rotate(360deg); } +@keyframes spin { + 100% { + -webkit-transform: rotate(360deg); + transform:rotate(360deg); } } @keyframes boxAnimator { @@ -1434,11 +1434,11 @@ cp { color: #ff00ff } * Also disabled on devices that may be slow to render new frames for performance optimization */ @media screen and - (prefers-reduced-motion: reduce), + (prefers-reduced-motion: reduce), (update: slow) { *, *::before, *::after { animation-duration: 0.001ms !important; animation-iteration-count: 1 !important; transition-duration: 0.001ms !important; } -} \ No newline at end of file +} diff --git a/assets/css/iconkit.css b/assets/css/iconkit.css index 96f0846a..d976b564 100644 --- a/assets/css/iconkit.css +++ b/assets/css/iconkit.css @@ -14,7 +14,7 @@ } body { - background-color: 555555; + background-color: #555; } p { @@ -144,9 +144,9 @@ input:focus, select:focus, textarea:focus, button:focus { } .supercenter { - position: absolute; - top: 50%; - left: 50%; + position: absolute; + top: 50%; + left: 50%; transform: translate(-50%,-50%); } @@ -194,7 +194,7 @@ input:focus, select:focus, textarea:focus, button:focus { padding: 10 10 10 10; overflow: auto; white-space: nowrap; - border-radius: 8px; + border-radius: 8px; } #symbols p { @@ -238,8 +238,8 @@ input:focus, select:focus, textarea:focus, button:focus { #generate { font-family: "Roboto"; border: rgba(0, 0, 0, 0); - background-color: #88FF33; - box-shadow: 2px 2px 3px #66AA22; + background-color: #8F3; + box-shadow: 2px 2px 3px #6A2; border-radius: 10px; color: black; padding: 10px 15px 10px 15px; @@ -259,8 +259,8 @@ input:focus, select:focus, textarea:focus, button:focus { .miniButton { font-family: "Roboto"; border: rgba(0, 0, 0, 0); - background-color: #88FF33; - box-shadow: 1.5px 1.5px 2px #66AA22; + background-color: #8F3; + box-shadow: 1.5px 1.5px 2px #6A2; border-radius: 10px; color: black; padding: 7px 9px 7px 9px; @@ -375,11 +375,11 @@ input:focus, select:focus, textarea:focus, button:focus { #colors::-webkit-scrollbar, #iconKitParent::-webkit-scrollbar { width: 9px; height: 10px; - background: rgba(0, 0, 0, 0.5); + background: rgba(0, 0, 0, 0.5); } #colors::-webkit-scrollbar-thumb, #iconKitParent::-webkit-scrollbar-thumb { - background: rgb(185, 185, 185); + background: rgb(185, 185, 185); } #iconKitParent { @@ -394,7 +394,7 @@ input:focus, select:focus, textarea:focus, button:focus { .iconColor div { width: 50px; - height: 50px; + height: 50px; } #gdfloor { @@ -423,13 +423,13 @@ input:focus, select:focus, textarea:focus, button:focus { } #iconprogressbar { - background: rgba(0, 0, 0, 0.5); + background: rgba(0, 0, 0, 0.5); height: 7px; margin-bottom: 12px; } #iconloading { - background: rgba(255, 255, 255, 0.5); + background: rgba(255, 255, 255, 0.5); height: 7px; width: 0%; } @@ -447,8 +447,8 @@ body::-webkit-scrollbar { } .popup { - position: fixed; - display: none; + position: fixed; + display: none; width: 100%; height: 100%; top: 0; left: 0; right: 0; bottom: 0; @@ -459,7 +459,7 @@ body::-webkit-scrollbar { .brownBox { border: 17px solid rgba(0, 0, 0, 0); border-radius: 25px; - background-color: #995533; + background-color: #953; border-image: url('../../assets/brownbox.png') 10% round; } @@ -686,4 +686,4 @@ input[type="range"]::-webkit-slider-thumb:active { background-image: url("../../ @-moz-keyframes spin { 100% { -moz-transform: rotate(360deg); } } @-webkit-keyframes spin { 100% { -webkit-transform: rotate(360deg); } } -@keyframes spin { 100% { -webkit-transform: rotate(360deg); transform:rotate(360deg); } } \ No newline at end of file +@keyframes spin { 100% { -webkit-transform: rotate(360deg); transform:rotate(360deg); } } diff --git a/classes/Level.js b/classes/Level.js index ad3b1065..17dc44a4 100644 --- a/classes/Level.js +++ b/classes/Level.js @@ -1,16 +1,19 @@ -const XOR = require(__dirname + "/../classes/XOR"); -const music = require(__dirname + "/../misc/music.json"); +"use strict"; +const XOR = require("./XOR") +const music = require("../misc/music.json") let orbs = [0, 0, 50, 75, 125, 175, 225, 275, 350, 425, 500] let length = ['Tiny', 'Short', 'Medium', 'Long', 'XL'] +// this can't be shortened with a loop let difficulty = { 0: 'Unrated', 10: 'Easy', 20: 'Normal', 30: 'Hard', 40: 'Harder', 50: 'Insane' } let demonTypes = { 3: "Easy", 4: "Medium", 5: "Insane", 6: "Extreme" } +let dailyLimit = 100000 class Level { constructor(levelInfo, server, download, author = []) { - this.name = levelInfo[2] || "-"; - this.id = levelInfo[1] || 0; - this.description = Buffer.from((levelInfo[3] || ""), "base64").toString() || "(No description provided)"; + this.name = levelInfo[2] || "-" + this.id = levelInfo[1] || 0 + this.description = Buffer.from((levelInfo[3] || ""), "base64").toString() || "(No description provided)" this.author = author[1] || "-" this.playerID = levelInfo[6] || 0 this.accountID = author[2] || 0 @@ -29,8 +32,8 @@ class Level { if (levelInfo[29]) this.updated = levelInfo[29] + (server.timestampSuffix || "") if (levelInfo[46]) this.editorTime = +levelInfo[46] || 0 if (levelInfo[47]) this.totalEditorTime = +levelInfo[47] || 0 - if (levelInfo[27]) this.password = levelInfo[27]; - this.version = +levelInfo[5] || 0; + if (levelInfo[27]) this.password = levelInfo[27] + this.version = +levelInfo[5] || 0 this.copiedID = levelInfo[30] || "0" this.twoPlayer = levelInfo[31] > 0 this.officialSong = +levelInfo[35] ? 0 : parseInt(levelInfo[12]) + 1 @@ -39,21 +42,24 @@ class Level { this.verifiedCoins = levelInfo[38] > 0 this.starsRequested = +levelInfo[39] || 0 this.ldm = levelInfo[40] > 0 - if (+levelInfo[41] > 100000) this.weekly = true - if (+levelInfo[41]) { this.dailyNumber = (+levelInfo[41] > 100000 ? +levelInfo[41] - 100000 : +levelInfo[41]); this.nextDaily = null; this.nextDailyTimestamp = null } + if (+levelInfo[41] > dailyLimit) this.weekly = true + if (+levelInfo[41]) { + this.dailyNumber = (+levelInfo[41] > dailyLimit ? +levelInfo[41] - dailyLimit : +levelInfo[41]) + this.nextDaily = null + this.nextDailyTimestamp = null + } this.objects = +levelInfo[45] || 0 - this.large = levelInfo[45] > 40000; + this.large = levelInfo[45] > 40000 this.cp = Number((this.stars > 0) + this.featured + this.epic) if (levelInfo[17] > 0) this.difficulty = (demonTypes[levelInfo[43]] || "Hard") + " Demon" if (levelInfo[25] > 0) this.difficulty = 'Auto' - this.difficultyFace = `${levelInfo[17] != 1 ? this.difficulty.toLowerCase() : `demon-${this.difficulty.toLowerCase().split(' ')[0]}`}${this.epic ? '-epic' : `${this.featured ? '-featured' : ''}`}` + this.difficultyFace = `${levelInfo[17] != 1 ? this.difficulty.toLowerCase() : `demon-${this.difficulty.toLowerCase().split(' ', 1)[0]}`}${this.epic ? '-epic' : `${this.featured ? '-featured' : ''}`}` if (this.password && this.password != 0) { - let xor = new XOR(); - let pass = xor.decrypt(this.password, 26364); - if (pass.length > 1) this.password = pass.slice(1); - else this.password = pass; + let xor = new XOR() + let pass = xor.decrypt(this.password, 26364) + this.password = pass.length > 1 ? pass.slice(1) : pass } if (server.onePointNine) { @@ -83,9 +89,9 @@ class Level { this.songSize = "0MB" this.songID = "Level " + this.officialSong } - + return this } } -module.exports = Level; \ No newline at end of file +module.exports = Level \ No newline at end of file diff --git a/classes/Player.js b/classes/Player.js index 51a70d79..fe4db924 100644 --- a/classes/Player.js +++ b/classes/Player.js @@ -1,4 +1,4 @@ -const colors = require('../iconkit/sacredtexts/colors.json'); +const colors = require('../iconkit/sacredtexts/colors.json') class Player { constructor(account) { diff --git a/classes/XOR.js b/classes/XOR.js index 87287ca0..51bbb4e6 100644 --- a/classes/XOR.js +++ b/classes/XOR.js @@ -1,5 +1,16 @@ +//https://nodejs.org/docs/latest/api/buffer.html#buffers-and-character-encodings +//both only work on "binary strings" and "URI-safe B64" +let toB64 = str => Buffer.from(str).toString('base64url') +let fromB64 = str => Buffer.from(str, 'base64').toString() + +const defKey = 37526 + module.exports = class XOR { - xor(str, key) { return String.fromCodePoint(...str.split('').map((char, i) => char.charCodeAt(0) ^ key.toString().charCodeAt(i % key.toString().length))) } - encrypt(str, key = 37526) { return Buffer.from(this.xor(str, key)).toString('base64').replace(/./gs, c => ({'/': '_', '+': '-'}[c] || c)); } - decrypt(str, key = 37526) { return this.xor(Buffer.from(str.replace(/./gs, c => ({'/': '_', '+': '-'}[c] || c)), 'base64').toString(), key) } -} + xor(str, key) { + key = key.toString() + return String.fromCodePoint(...str.split('') + .map((c, i) => c.charCodeAt(0) ^ key.charCodeAt(i % key.length))) + } + encrypt(str, key = defKey) { return toB64(this.xor(str, key)) } + decrypt(str, key = defKey) { return this.xor(fromB64(str), key) } +} \ No newline at end of file diff --git a/html/achievements.html b/html/achievements.html index ba6858fa..4b37a183 100644 --- a/html/achievements.html +++ b/html/achievements.html @@ -21,23 +21,23 @@

Filters


-

Reward

+

Reward

-
+
-

Requirement

+

Requirement

-
+
-

Game

+

Game

-
+
@@ -59,7 +59,7 @@

Game

- +
@@ -79,10 +79,10 @@

Achievements

- - + + \ No newline at end of file diff --git a/html/analyze.html b/html/analyze.html index a5d7cfaa..0dd89c63 100644 --- a/html/analyze.html +++ b/html/analyze.html @@ -3,11 +3,12 @@ - + - + + @@ -100,25 +101,27 @@

Level Data

- + diff --git a/html/api.html b/html/api.html index d85368ea..cf86340b 100644 --- a/html/api.html +++ b/html/api.html @@ -3,11 +3,11 @@ GD Level Browser API - - + + - + @@ -35,22 +35,22 @@

Hi there!

// smooth scrolling through anchors document.querySelectorAll('a[href^="#"]').forEach(anchor => { anchor.addEventListener('click', function(e) { - e.preventDefault(); + e.preventDefault() document.querySelector(this.getAttribute('href')).scrollIntoView({ behavior: 'smooth' }); }); }); - + // menu button document.getElementById('menu-btn').onclick = function(){ - document.getElementsByClassName('header-links')[0].classList.toggle('hid'); + document.getElementsByClassName('header-links')[0].classList.toggle('hid') document.getElementById('menu-btn').classList.toggle('active'); } - + for(let i = 0; i < document.getElementsByClassName('header-link').length; i++){ document.getElementsByClassName('header-link')[i].onclick = function(){ - document.getElementsByClassName('header-links')[0].classList.toggle('hid'); + document.getElementsByClassName('header-links')[0].classList.toggle('hid') document.getElementById('menu-btn').classList.toggle('active'); } } diff --git a/html/api_old.html b/html/api_old.html index 45125f2c..92e81c34 100644 --- a/html/api_old.html +++ b/html/api_old.html @@ -3,11 +3,11 @@ GD Level Browser API - - + + - + @@ -19,7 +19,7 @@
@@ -186,7 +186,7 @@

Levels

- +

Profiles

/api/profile/username-or-id

@@ -244,7 +244,7 @@

Profiles

- +

Searching

/api/search/search-query

@@ -540,10 +540,10 @@

Comments and Profile Posts

Commenting (usually broken)

POST: /postComment

- +

Leaves a comment on a level. This one is a POST request!

*Commenting has a rate limit of 15 seconds

- +

Parameters (6)

@@ -555,7 +555,7 @@

Commenting (usually broken)

percent: The percent shown on the comment (optional)

color: If the comment should have a special pink color on GDBrowser (optional)

- +

Example

@@ -571,7 +571,7 @@

Commenting (usually broken)


If a status of 200 is returned, then the comment was successfully posted. Otherwise, a 400 will return with an error message.

- +
@@ -581,9 +581,9 @@

Commenting (usually broken)

Profile Posting (usually broken)

POST: /postProfileComment

- +

Leaves a profile post. This one is a POST request!

- +

Parameters (5)

@@ -593,7 +593,7 @@

Profile Posting (usually broken)

password: Your password (as plain text)

color: If the comment should have a special pink color on GDBrowser (optional)

- +

Example

@@ -607,7 +607,7 @@

Profile Posting (usually broken)


If a status of 200 is returned, then the profile post was successfully posted. Otherwise, a 400 will return with an error message.

- +
@@ -618,9 +618,9 @@

Profile Posting (usually broken)

Liking (usually broken)

POST: /like

- +

Likes/dislikes level, comment, or post. This one is a POST request!

- +

Parameters (6)

@@ -631,7 +631,7 @@

Liking (usually broken)

accountID: Your account ID

password: Your password (as plain text)

- +

Example

@@ -649,7 +649,7 @@

Liking (usually broken)

A status of 200 will return if everything goes well, otherwise a 400 will return with an error message.
Liking a comment multiple times on the same account will return a 200, but not actually increase the in-game like counter.

- +
@@ -665,7 +665,7 @@

Messages (usually broken)

/sendMessage (sends a message)

I decided to put all 4 of these requests in one section because they're fairly similar ¯\_(ツ)_/¯

- +

Parameters

@@ -687,7 +687,7 @@

Messages (usually broken)

message: The content of the message, max 300 characters

color: If the message should have a special pink color on GDBrowser (optional)

- +

Example

@@ -699,7 +699,7 @@

Messages (usually broken)

&accountID=106255
&password=KitsuneColon333

- +

Read message with ID of 177013:

POST /messages/177013
?accountID=106255
@@ -712,7 +712,7 @@

Messages (usually broken)

&id=177013
&password=KitsuneColon333

- +

Send "Hello!" to Tubular9:

POST /sendMessage
?accountID=106255
@@ -877,22 +877,22 @@

Icons

// smooth scrolling through anchors document.querySelectorAll('a[href^="#"]').forEach(anchor => { anchor.addEventListener('click', function(e) { - e.preventDefault(); + e.preventDefault() document.querySelector(this.getAttribute('href')).scrollIntoView({ behavior: 'smooth' }); }); }); - + // menu button document.getElementById('menu-btn').onclick = function(){ - document.getElementsByClassName('header-links')[0].classList.toggle('hid'); + document.getElementsByClassName('header-links')[0].classList.toggle('hid') document.getElementById('menu-btn').classList.toggle('active'); } - + for(let i = 0; i < document.getElementsByClassName('header-link').length; i++){ document.getElementsByClassName('header-link')[i].onclick = function(){ - document.getElementsByClassName('header-links')[0].classList.toggle('hid'); + document.getElementsByClassName('header-links')[0].classList.toggle('hid') document.getElementById('menu-btn').classList.toggle('active'); } } diff --git a/html/assets.html b/html/assets.html index 20644c28..5b2c9ae4 100644 --- a/html/assets.html +++ b/html/assets.html @@ -3,13 +3,13 @@ - + diff --git a/html/boomlings.html b/html/boomlings.html index 04ace9c0..4eecdbc0 100644 --- a/html/boomlings.html +++ b/html/boomlings.html @@ -1,10 +1,10 @@ Boomlings Leaderboard - + - + @@ -22,19 +22,19 @@

Boomlings Leaderboard

- +
- +
- +
- +
@@ -42,14 +42,14 @@

Boomlings Leaderboard

- - + + - + @@ -28,16 +28,16 @@

GD Password

- - + + - +
- +
- +
@@ -69,39 +69,39 @@

- +
- +
- +
- +
- +
- +
- +
- +
- +
@@ -114,15 +114,15 @@

- +
- +
@@ -130,9 +130,9 @@

- - - + + + - diff --git a/html/demon.html b/html/demon.html index 9cb842b1..c2395beb 100644 --- a/html/demon.html +++ b/html/demon.html @@ -1,9 +1,9 @@ Demon Leaderboard - + - + @@ -24,22 +24,22 @@

Credits

Usernames may differ from what is used in GD

- +
- +
- +
- +
@@ -47,35 +47,35 @@

- +
- +
- +
- +
- - + +
- +
- + - + @@ -16,7 +16,7 @@
- +
- +
- - + +
@@ -79,17 +79,17 @@

Quick Search

- + - +
- - - + + +
- - - + + +
@@ -97,56 +97,56 @@

Filters

-

N/A

-

Easy

-

Normal

-

Hard

-

Harder

-

Insane

+

N/A

+

Easy

+

Normal

+

Hard

+

Harder

+

Insane

-

Demon

+

Demon

- - + + -

Auto

+

Auto

- +

Tiny

Short

Medium

Long

XL

-
+
- +
- - - + + +
- + - + @@ -15,34 +15,34 @@
- +
- +
- +


- +
- + - + @@ -26,43 +26,43 @@

Add server

Please note that I only add relatively large servers to the list. Servers which are inactive or have few levels/members will not be accepted.

- + - +

GD Private Servers

-
+
- +
- +
- +
- +
- +
- + - + @@ -18,76 +18,76 @@
- +
- +
- +
- +
- + - +
- +
- +
-
- +
+
- +

(Hover over a setting for information)

- - + + @@ -45,14 +45,14 @@

Settings<

Enable 2.2 icons?

The newest update for Geometry Dash Lite revealed 500 new icons across all forms. Enabling this setting will reveal all these icons, however they will be lower quality since no UHD textures were provided.

THIS WILL REVEAL EVERY ICON.
PRESS CANCEL IF YOU DON'T WANT TO BE SPOILED!!!

- - - + + + -
- +
+
@@ -60,14 +60,14 @@
@@ -101,35 +101,36 @@

User Search< - - - + + + - + @@ -24,7 +24,7 @@

Level Info


GD Version: [[GAMEVERSION]] [[OBJECTINFO]][[REQUESTED]]

- + @@ -34,7 +34,7 @@

Saved!

[[NAME]] has been added to your saved levels list.

- + @@ -44,8 +44,8 @@

Delete Level

Are you sure you want to delete this level from your saved levels list?

- - + + @@ -55,15 +55,15 @@

Level Analysis

Level analysis is currently blocked by RobTop. We don't know when or if it will be re-enabled.
(click to try anyways)

- +
- +
- +
@@ -93,31 +93,31 @@

[[NAME]]

By [[AUTHOR]]

- - - + + +


#[[DAILYNUMBER]]

- +

[[DIFFICULTY]]

-

[[STARS]]


-

[[DIAMONDS]]

-

#[[DEMONLIST]]

+

[[STARS]]


+

[[DIAMONDS]]

+

#[[DEMONLIST]]

- +

[[DOWNLOADS]]


- +

[[LIKES]]


- +

[[LENGTH]]


- +

[[ORBS]]

@@ -129,16 +129,16 @@

[[ORBS]]

[[SONGNAME]]

By: [[SONGAUTHOR]]

- + --> +
- +
@@ -146,20 +146,20 @@

SongID: [[SONGID]]   

- +
-
-
- -
-
-
+
+
+ +
+
+
- +

- + - + @@ -29,9 +29,9 @@

GD Password

- - @@ -40,7 +40,7 @@

GD Password

- +
@@ -52,25 +52,25 @@

Messages
- + - -
- +
-
-
@@ -84,19 +84,19 @@

Delete

Are you sure you want to delete this message?

- - + + @@ -111,9 +111,9 @@

Bulk Delete

Are you sure you want to delete ?

- - @@ -121,12 +121,12 @@

Bulk Delete

Delete

Deleting ...

- + @@ -136,16 +136,16 @@

Delete

- - - + @@ -176,19 +176,19 @@

- +

Sending...

@@ -197,15 +197,15 @@

- +
- +
- +
@@ -213,18 +213,18 @@

- + - + @@ -15,7 +15,7 @@

RobTop's Purgatory

 

- +
- +
- +
- +
- +
- +
- +
- -
+ +
- + @@ -26,16 +26,16 @@

GD Password

- - + + - +
- +
- +
- +
-

[[RANK]]

+

[[RANK]]

- [[USERNAME]]

- +

- [[STARS]] - [[DIAMONDS]] - [[COINS]] - [[USERCOINS]] - [[DEMONS]] - + [[STARS]] + [[DIAMONDS]] + [[COINS]] + [[USERCOINS]] + [[DEMONS]] +

- - + +

Account ID: [[ACCOUNTID]]
Player ID: [[PLAYERID]]

- - - - - + + + + + - +
- - - + + +
- +
- +
- +
- +
@@ -153,9 +153,9 @@

- - - + + + diff --git a/html/search.html b/html/search.html index ef0cbe23..60aa7579 100644 --- a/html/search.html +++ b/html/search.html @@ -1,9 +1,9 @@ Level Search - + - + @@ -18,15 +18,15 @@

Jump to Page


- - + +
- +
- +
- +
@@ -37,8 +37,8 @@

Delete All

Delete all saved online levels?
Levels will be cleared from your browser.

- - + + @@ -49,22 +49,22 @@

Random Level

A random level cannot be picked with your current search filters! This is because there is no way to tell how many results were found, due to the GD servers inaccurately saying there's 9999.

- +
- +
- +
- +
@@ -76,61 +76,62 @@

- +
- +
- +
- +
- +
- +