added custom page for gauntlets

This commit is contained in:
Tim 2022-05-27 15:22:25 +02:00
commit 8691f4e866
2719 changed files with 35884 additions and 0 deletions

70
.github/workflows/analyse.yml vendored Normal file
View file

@ -0,0 +1,70 @@
# For most projects, this workflow file will not need changing; you simply need
# to commit it to your repository.
#
# You may wish to alter this file to override the set of languages analyzed,
# or to provide custom queries or build logic.
#
# ******** NOTE ********
# We have attempted to detect the languages in your repository. Please check
# the `language` matrix defined below to confirm you have the correct set of
# supported CodeQL languages.
#
name: "CodeQL"
on:
push:
branches: [ master ]
pull_request:
# The branches below must be a subset of the branches above
branches: [ master ]
schedule:
- cron: '42 17 * * 4'
jobs:
analyze:
name: Analyze
runs-on: ubuntu-latest
permissions:
actions: read
contents: read
security-events: write
strategy:
fail-fast: false
matrix:
language: [ 'javascript' ]
# CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python', 'ruby' ]
# Learn more about CodeQL language support at https://git.io/codeql-language-support
steps:
- name: Checkout repository
uses: actions/checkout@v2
# Initializes the CodeQL tools for scanning.
- name: Initialize CodeQL
uses: github/codeql-action/init@v1
with:
languages: ${{ matrix.language }}
# If you wish to specify custom queries, you can do so here or in a config file.
# By default, queries listed here will override any specified in a config file.
# Prefix the list here with "+" to use these queries and those in the config file.
# queries: ./path/to/local/query, your-org/your-repo/queries@main
# Autobuild attempts to build any compiled languages (C/C++, C#, or Java).
# If this step fails, then you should remove it and run the build manually (see below)
- name: Autobuild
uses: github/codeql-action/autobuild@v1
# Command-line programs to run using the OS shell.
# 📚 https://git.io/JvXDl
# ✏️ If the Autobuild fails above, remove it and uncomment the following three lines
# and modify them (or add more) to build your code if your project
# uses a compiled language
#- run: |
# make bootstrap
# make release
- name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@v1

67
.gitignore vendored Normal file
View file

@ -0,0 +1,67 @@
# Ew
extra
# Package manager lockfiles
package-lock.json
yarn.lock
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
# Runtime data
pids
*.pid
*.seed
*.pid.lock
# Directory for instrumented libs generated by jscoverage/JSCover
lib-cov
# Coverage directory used by tools like istanbul
coverage
# nyc test coverage
.nyc_output
# Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files)
.grunt
# Bower dependency directory (https://bower.io/)
bower_components
# node-waf configuration
.lock-wscript
# Compiled binary addons (https://nodejs.org/api/addons.html)
build/Release
# Dependency directories
node_modules/
jspm_packages/
# Optional npm cache directory
.npm
# Optional eslint cache
.eslintcache
# Optional REPL history
.node_repl_history
# Output of 'npm pack'
*.tgz
# Yarn Integrity file
.yarn-integrity
# dotenv environment variables file
.env
.env.*
# Editors
.idea
.vscode

21
LICENSE Normal file
View file

@ -0,0 +1,21 @@
MIT License
Copyright (c) 2020 Colon
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

170
README.md Normal file
View file

@ -0,0 +1,170 @@
hi, this is colon from the future.
what the FUCK was wrong with me back then???? seriously this is some of the worst code i've ever seen
welp, here's the readme. but you've been warned,,,
# GDBrowser
Uh... so I've never actually used GitHub before this. But I'll try to explain everything going on here.
Sorry for my messy code. It's why I was skeptical about making this open source, but you know what, the code runs fine in the end.
## How do I run this?
If you're just here to use GDBrowser locally because the site is down or blocked or restricted or god knows what, this is the only part you really need to read
To run GDBrowser locally:
1) Install [node.js](https://nodejs.org/en/download/) if you don't already have it
2) Clone/download this repository
3) Open cmd/powershell/terminal in the main folder (with index.js)
4) Type `npm i` to flood your hard drive with code that's 99% useless
5) Type `node index` to run the web server
6) GDBrowser is now running locally at http://localhost:2000
If you want to disable rate limits, ip forwarding, etc you can do so by modifying `settings.js`. Doing this is probably a good idea if you feel like obliterating Rob's servers for some reason. (please don't)
## Using this for a GDPS?
~~I mean, sure. Why not.~~
Hold up, wait a minute... private servers are an official feature now!
If you would like to add your GDPS to GDBrowser, [fill out this quick form](https://forms.gle/kncuRqyKykQX42QD7) and I'll be happy to add it (provided the server is relatively large and active)
If you 100% insist on adding a private server to your own magical little fork, you can do so by adding it to **servers.json**. Simply add a new object to the array with the following information:
| identifier | description |
|:----------------:|:-----------------------------:|
| `name` | The display name of the server |
| `link` | The server's website URL (unrelated to the actual endpoint) |
| `author` | The creator(s) of the server |
| `authorLink` | The URL to open when clicking on the creator's name |
| `id` | An ID for the server, also used as the subdomain (e.g. `something` would become `something.gdbrowser.com`) |
| `endpoint` | The actual endpoint to ~~spam~~ send requests to (e.g. `http://boomlings.com/database/` - make sure it ends with a slash!) |
There's also a few optional values for fine-tuning. I'll add more over time
| identifier | description | type |
|:----------------:|:-----------------------------:|:----:|
| `timestampSuffix` | A string to append at the end of timestamps. Vanilla GD uses " ago" | string |
| `demonList` | The URL of the server's Demon List API, if it has one (e.g. `http://pointercrate.com/` - make sure it ends with a slash!) | string |
| `disabled` | An array of menu buttons to "disable" (mappacks, gauntlets, daily, weekly, etc). They appear greyed out but are still clickable. | array |
| `pinned` | "Pins" the server to the top of the GDPS list. It appears above all unpinned servers and is not placed in alphabetical order. | bool |
| `onePointNine` | Makes a bunch of fancy changes to better fit 1.9 servers. (removes orbs/diamonds, hides some pointless buttons, etc) | bool |
| `weeklyLeaderboard` | Enables the lost but not forgotten Weekly Leaderboard, for servers that still milk it | bool |
| `substitutions` | A list of parameter substitutions, because some servers rename/obfuscate them. (e.g. `{ "levelID": "oiuyhxp4w9I" }`) | object |
| `overrides` | A list of endpoint substitutions, because some servers use renamed or older versions. (e.g. `{ "getGJLevels21": "dorabaeChooseLevel42" }`) | object |
# Folders
GDBrowser has a lot of folders. [citation needed]
I pride myself in keeping my files neat, without doing the whole `src/main/data/stuff/code/homework/newfolder/util/actualcode` garbage
Most folders contain exactly what you'd expect, but here's some in-depth info in case you're in the dark.
## API
This is where all the backend stuff happens! Yipee!
They're all fairly similar. Fetch something, parse the response, and serve it in a crisp and non-intimidating JSON. This is probably what you came for.
## Assets
Assets! Assets everywhere!
All the GD stuff was ripped straight from the GD spritesheets via [Absolute's texture splitter hack](https://youtu.be/pYQgIyNhow8). If you want a nice categorized version, [I've done all the dirty work for you.](https://www.mediafire.com/file/4d99bw1zhwcl507/textures.zip/file)
I'd explain what's in all the subfolders but it's pretty obvious. I tried my best to organize everything nicely.
## Classes
What's a class you ask? Good question.
I guess the best way to put it is uh... super fancy functions???
Level.js parses the server's disgusting response and sends back a nice object with all the level info
XOR.js encrypts/decrypts stuff like GD passwords
## HTML
The HTML files! Nothing too fancy, since it can all be seen directly from gdbrowser. Note that profile.html and level.html (and some parts of home.html) have [[VARIABLES]] (name, id, etc) replaced by the server when they're sent.
## Misc
Inevitable misc folder
**Level Analysis Stuff (in a separate folder)**
| name | description |
|:----:|:-----------:|
| `blocks.json` | The object IDs in the different 'families' of blocks |
| `colorProperties.json` | Color channel cheatsheet |
| `initialProperties.json` | Level settings cheatsheet |
| `objectProperties.json` | Object property cheatsheet. Low budget version of [AlFas' one](https://github.com/AlFasGD/GDAPI/blob/master/GDAPI/GDAPI/Enumerations/GeometryDash/ObjectProperty.cs) |
| `objects.json` | IDs for portals, orbs, triggers, and misc stuff |
**Everything Else**
| name | description |
|:----:|:-----------:|
| `achievements.json` | List of all GD/meltdown/subzero/etc achievements. `parseAchievementPlist.js` automatically creates this file |
| `achievementTypes.json` | An object containing different categories of achievements (stars, shards, vault, etc) and how to identify them |
| `credits.json` | Credits! (shown on the homepage) |
| `dragscroll.js` | Used on several pages for drag scrolling |
| `global.js` | Excecuted on most pages. Used for the 'page isn't wide enough' message, back button, icons, and a few other things |
| `music.json` | An array of the official GD tracks (name, artist) |
| `sampleIcons.json` | A pool of icons, one of which will randomly appear when visiting the icon kit. Syntax is [Name, ID, Col1, Col2, Glow] |
| `secretStuff.json` | GJP goes here, needed for level leaderboards. Not included in the repo for obvious reasons |
---
happy gdbrowsing and god bless.

339
api/analyze.js Normal file
View file

@ -0,0 +1,339 @@
const zlib = require('zlib')
const blocks = require('../misc/analysis/blocks.json')
const colorStuff = require('../misc/analysis/colorProperties.json')
const init = require('../misc/analysis/initialProperties.json')
const properties = require('../misc/analysis/objectProperties.json')
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 || "")
}
}
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 {
zlib.unzip(levelString, (err, buffer) => {
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);
});
}
}
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])
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 = {};
// 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];
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];
}
}
return robtop_obj;
}
function analyze_level(level, rawData) {
let response = {};
let blockCounts = {}
let miscCounts = {}
let triggerGroups = []
let highDetail = 0
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);
// funny enough, shift effects the original id list
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;
});
}
const data = rawData.split(";");
const header = data.shift();
let level_portals = [];
let level_coins = [];
let level_text = [];
let orb_array = {};
let trigger_array = {};
let last = 0;
const obj_length = data.length;
for (let i = 0; i < obj_length; ++i) {
obj = parse_obj(data[i], ',', properties);
let id = obj.id
if (id in ids.portals) {
obj.portal = ids.portals[id];
level_portals.push(obj);
} else if (id in ids.coins) {
obj.coin = ids.coins[id];
level_coins.push(obj);
} else if (id in ids.orbs) {
obj.orb = ids.orbs[id];
if (obj.orb in orb_array) {
orb_array[obj.orb]++;
} else {
orb_array[obj.orb] = 1;
}
} else if (id in ids.triggers) {
obj.trigger = ids.triggers[id];
if (obj.trigger in trigger_array) {
trigger_array[obj.trigger]++;
} else {
trigger_array[obj.trigger] = 1;
}
}
if (obj.message) {
level_text.push(obj)
}
if (obj.triggerGroups) obj.triggerGroups.split('.').forEach(x => triggerGroups.push(x))
if (obj.highDetail == 1) highDetail += 1
if (id in misc_objects) {
const name = misc_objects[id];
if (name in miscCounts) {
miscCounts[name][0] += 1;
} else {
miscCounts[name] = [1, ids.misc[name][0]];
}
}
if (id in block_ids) {
const name = block_ids[id];
if (name in blockCounts) {
blockCounts[name] += 1;
} else {
blockCounts[name] = 1;
}
}
if (obj.x) { // sometimes the field doesn't exist
last = Math.max(last, obj.x);
}
if (obj.trigger == "Alpha") { // invisible triggers
alphaTriggers.push(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))
})
response.level = {
name: level.name, id: level.id, author: level.author, playerID: level.playerID, accountID: level.accountID, large: level.large
}
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))
response.coinsVerified = level.verifiedCoins
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.triggers = trigger_array
response.triggers.total = Object.values(trigger_array).reduce((a, x) => a + x, 0);
response.triggerGroups = {}
response.blocks = sortObj(blockCounts)
response.misc = sortObj(miscCounts, '0')
response.colors = []
triggerGroups.forEach(x => {
if (response.triggerGroups['Group ' + x]) response.triggerGroups['Group ' + x] += 1
else response.triggerGroups['Group ' + x] = 1
})
response.triggerGroups = sortObj(response.triggerGroups)
let triggerKeys = Object.keys(response.triggerGroups).map(x => Number(x.slice(6)))
response.triggerGroups.total = triggerKeys.length
// 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) + "%"])
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;
}
function parse_header(header) {
let response = {};
response.settings = {};
response.colors = [];
const header_keyed = parse_obj(header, ',', init.values, true);
Object.keys(header_keyed).forEach(x => {
let val = init.values[x]
let name = val[0]
let property = header_keyed[x]
switch (val[1]) {
case 'list':
val = init[(val[0] + "s")][property];
break;
case 'number':
val = Number(property);
break;
case 'bump':
val = Number(property) + 1;
break;
case 'bool':
val = property != "0";
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];
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;
}
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);
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()
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;
}
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 (colorStuff.channels[colorObj.channel]) colorObj.channel = colorStuff.channels[colorObj.channel]
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.pColor == "-1") delete colorObj.pColor
if (colorObj.blending) colorObj.blending = true
if (colorObj.copiedHSV) {
let hsv = colorObj.copiedHSV.split("a")
colorObj.copiedHSV = {}
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
}
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) } );
response.colors = specialColors.concat(regularColors)
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.alternateLine == 2) response.settings.alternateLine = true
else response.settings.alternateLine = false
Object.keys(response.settings).filter(k => {
// this should be parsed into color list instead
if (k.includes('legacy')) delete response.settings[k];
});
delete response.settings['colors'];
return response;
}

82
api/comments.js Normal file
View file

@ -0,0 +1,82 @@
const Player = require('../classes/Player.js')
module.exports = async (app, req, res) => {
if (req.offline) return res.sendError()
let count = +req.query.count || 10
if (count > 1000) count = 1000
let params = {
userID : req.params.id,
accountID : req.params.id,
levelID: req.params.id,
page: +req.query.page || 0,
count,
mode: req.query.hasOwnProperty("top") ? "1" : "0",
}
let path = "getGJComments21"
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) {
if (err) return res.sendError()
comments = body.split('|')
comments = comments.map(x => x.split(':'))
comments = comments.map(x => x.map(x => app.parseResponse(x, "~")))
if (req.query.type == "profile") comments.filter(x => x[0][2])
else comments = comments.filter(x => x[0] && x[0][2])
if (!comments.length) return res.status(204).send([])
let pages = body.split('#')[1].split(":")
let lastPage = +Math.ceil(+pages[0] / +pages[2]);
let commentArray = []
comments.forEach((c, i) => {
var x = c[0] //comment info
var y = c[1] //account info
if (!x[2]) return;
let comment = {}
comment.content = Buffer.from(x[2], 'base64').toString();
comment.ID = x[6]
comment.likes = +x[4]
comment.date = (x[9] || "?") + req.timestampSuffix
if (comment.content.endsWith("⍟") || comment.content.endsWith("☆")) {
comment.content = comment.content.slice(0, -1)
comment.browserColor = true
}
if (req.query.type != "profile") {
let commentUser = new Player(y)
Object.keys(commentUser).forEach(k => {
comment[k] = commentUser[k]
})
comment.levelID = x[1] || req.params.id
comment.playerID = x[3] || 0
comment.color = (comment.playerID == "16" ? "50,255,255" : x[12] || "255,255,255")
if (x[10] > 0) comment.percent = +x[10]
comment.moderator = +x[11] || 0
app.userCache(req.id, comment.accountID, comment.playerID, comment.username)
}
if (i == 0 && req.query.type != "commentHistory") {
comment.results = +pages[0];
comment.pages = lastPage;
comment.range = `${+pages[1] + 1} to ${Math.min(+pages[0], +pages[1] + +pages[2])}`
}
commentArray.push(comment)
})
return res.send(commentArray)
})
}

110
api/download.js Normal file
View file

@ -0,0 +1,110 @@
const request = require('request')
const fs = require('fs')
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()
}
if (req.offline) {
if (!api && levelID < 0) return res.redirect('/')
return 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, "")
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()
}
let authorData = body.split("#")[3] // daily/weekly only, most likely
let levelInfo = app.parseResponse(body)
let level = new Level(levelInfo, req.server, true)
if (!level.id) return rejectLevel()
let foundID = app.accountCache[req.id][Object.keys(app.accountCache[req.id]).find(x => app.accountCache[req.id][x][1] == level.playerID)]
if (foundID) foundID = foundID.filter(x => x != level.playerID)
req.gdRequest(authorData ? "" : 'getGJUsers20', { str: level.playerID }, function (err1, res1, b1) {
let gdSearchResult = authorData ? "" : app.parseResponse(b1)
req.gdRequest(authorData ? "" : 'getGJUserInfo20', { targetAccountID: gdSearchResult[16] }, function (err2, res2, b2) {
if (err2 && (foundID || authorData)) {
let authorInfo = foundID || authorData.split(":")
level.author = authorInfo[1] || "-"
level.accountID = authorInfo[0] && authorInfo[0].includes(",") ? "0" : authorInfo[0]
}
else if (!err && b2 != '-1') {
let account = app.parseResponse(b2)
level.author = account[1] || "-"
level.accountID = gdSearchResult[16]
}
else {
level.author = "-"
level.accountID = "0"
}
if (level.author != "-") app.userCache(req.id, level.accountID, level.playerID, level.author)
req.gdRequest('getGJSongInfo', { songID: level.customSong }, function (err, resp, songRes) {
level = level.getSongInfo(app.parseResponse(songRes, '~|~'))
level.extraString = levelInfo[36]
level.data = levelInfo[4]
if (req.isGDPS) level.gdps = (req.onePointNine ? "1.9/" : "") + req.server.id
if (analyze) return app.run.analyze(app, req, res, level)
function sendLevel() {
if (api) return res.send(level)
else return fs.readFile('./html/level.html', 'utf8', function (err, data) {
let html = data;
let variables = Object.keys(level)
variables.forEach(x => {
let regex = new RegExp(`\\[\\[${x.toUpperCase()}\\]\\]`, "g")
html = html.replace(regex, app.clean(level[x]))
})
return res.send(html)
})
}
if (levelID < 0) {
req.gdRequest('getGJDailyLevel', { weekly: levelID == -2 ? "1" : "0" }, function (err, resp, dailyInfo) {
if (err || dailyInfo == -1) return sendLevel()
let dailyTime = dailyInfo.split("|")[1]
level.nextDaily = +dailyTime
level.nextDailyTimestamp = Math.round((Date.now() + (+dailyTime * 1000)) / 100000) * 100000
return sendLevel()
})
}
else if (req.server.demonList && level.difficulty == "Extreme Demon") {
request.get(req.server.demonList + 'api/v2/demons/?name=' + level.name.trim(), function (err, resp, demonList) {
if (err) return sendLevel()
let demon = JSON.parse(demonList)
if (demon[0] && demon[0].position) level.demonList = demon[0].position
return sendLevel()
})
}
else return sendLevel()
})
})
})
})
}

22
api/gauntlets.js Normal file
View file

@ -0,0 +1,22 @@
let cache = {}
let gauntletNames = ["Fire", "Ice", "Poison", "Shadow", "Lava", "Bonus", "Chaos", "Demon", "Time", "Crystal", "Magic", "Spike", "Monster", "Doom", "Death"]
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
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 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()}
res.send(gauntletList)
})
}

View file

@ -0,0 +1,47 @@
const {GoogleSpreadsheet} = require('google-spreadsheet');
const sheet = new GoogleSpreadsheet('1ADIJvAkL0XHGBDhO7PP9aQOuK3mPIKB2cVPbshuBBHc'); // accurate leaderboard spreadsheet
let indexes = ["stars", "coins", "demons", "diamonds"]
let forms = ['cube', 'ship', 'ball', 'ufo', 'wave', 'robot', 'spider']
let lastIndex = [{"stars": 0, "coins": 0, "demons": 0}, {"stars": 0, "coins": 0, "demons": 0, "diamonds": 0}]
let caches = [{"stars": null, "coins": null, "demons": null, "diamonds": null}, {"stars": null, "coins": null, "demons": null, "diamonds": null}, {"stars": null, "coins": null, "demons": null, "diamonds": null}] // 0 for JSON, 1 for mods, 2 for GD
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)
})
}

View file

@ -0,0 +1,43 @@
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")
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")
let info = body.split(" ").filter(x => x.includes(";"))
let users = []
info.forEach((x, y) => {
let user = x.split(";")
let scores = user[2]
let visuals = user[3]
user = {
rank: y+1,
name: user[0],
ID: user[1],
level: +scores.slice(1, 3),
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),
unknownVisual: +visuals.slice(0, 2),
unknownScore: +scores.slice(0, 1),
raw: x
}
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)
})
return res.send(users)
})
}

View file

@ -0,0 +1,53 @@
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))
})
}

View file

@ -0,0 +1,33 @@
const Player = require('../../classes/Player.js')
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 > 10000) amount = 10000
else amount = count;
}
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
}
req.gdRequest('getGJScores20', params, function(err, resp, body) {
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))
})
}

69
api/level.js Normal file
View file

@ -0,0 +1,69 @@
const request = require('request')
const fs = require('fs')
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()
}
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 (analyze || req.query.hasOwnProperty("download")) return app.run.download(app, req, res, api, levelID, analyze)
req.gdRequest('getGJLevels21', { str: levelID, type: 0 }, function (err, resp, body) {
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];
song = app.parseResponse(song, '~|~')
let levelInfo = app.parseResponse(preRes.find(x => x.startsWith(`1:${levelID}`)) || preRes[0])
let level = new Level(levelInfo, req.server, false, author).getSongInfo(song)
if (!level.id) return rejectLevel()
if (req.isGDPS) level.gdps = (req.onePointNine ? "1.9/" : "") + req.server.id
if (level.author != "-") app.userCache(req.id, level.accountID, level.playerID, level.author)
function sendLevel() {
if (api) return res.send(level)
else return fs.readFile('./html/level.html', 'utf8', function (err, data) {
let html = data;
let filteredSong = level.songName.replace(/[^ -~]/g, "") // strip off unsupported characters
level.songName = filteredSong || level.songName
let variables = Object.keys(level)
variables.forEach(x => {
let regex = new RegExp(`\\[\\[${x.toUpperCase()}\\]\\]`, "g")
html = html.replace(regex, app.clean(level[x]))
})
if (req.server.downloadsDisabled) html = html.replace('id="additional" class="', 'id="additional" class="downloadDisabled ')
.replace('analyzeBtn"', 'analyzeBtn" style="filter: opacity(30%)"')
return res.send(html)
})
}
if (req.server.demonList && level.difficulty == "Extreme Demon") {
request.get(req.server.demonList + 'api/v2/demons/?name=' + level.name.trim(), function (err, resp, demonList) {
if (err) return sendLevel()
let demon = JSON.parse(demonList)
if (demon[0] && demon[0].position) level.demonList = demon[0].position
return sendLevel()
})
}
else return sendLevel()
})
}

43
api/mappacks.js Normal file
View file

@ -0,0 +1,43 @@
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
let params = { count: 250, page: 0 }
let packs = []
function mapPackLoop() {
req.gdRequest('getGJMapPacks21', params, function (err, resp, body) {
if (err) return res.sendError()
let newPacks = body.split('#')[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!!!
if (newPacks.length == 10) {
params.page++
return mapPackLoop()
}
let mappacks = packs.map(x => ({ // "packs.map()" laugh now please
id: +x[1],
name: x[2],
levels: x[3].split(","),
stars: +x[4],
coins: +x[5],
difficulty: difficulties[+x[6]] || "unrated",
barColor: x[7],
textColor: x[8]
}))
if (app.config.cacheMapPacks) cache[req.id] = {data: mappacks, indexed: Date.now()}
return res.send(mappacks)
})
}
mapPackLoop()
}

View file

@ -0,0 +1,23 @@
module.exports = async (app, req, res) => {
if (req.method !== 'POST') return res.status(405).send("Method not allowed.")
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!")
let params = {
accountID: req.body.accountID,
targetAccountID: req.body.accountID,
gjp: app.xor.encrypt(req.body.password, 37526),
}
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)
let count = app.parseResponse(body)[38]
if (!count) return res.status(400).send("Error fetching unread messages!")
else res.send(count)
})
}

View file

@ -0,0 +1,24 @@
module.exports = async (app, req, res) => {
if (req.method !== 'POST') return res.status(405).send("Method not allowed.")
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!")
let params = {
accountID: req.body.accountID,
gjp: app.xor.encrypt(req.body.password, 37526),
messages: Array.isArray(req.body.id) ? req.body.id.map(x => x.trim()).join(",") : req.body.id,
}
let deleted = params.messages.split(",").length
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!`)
app.trackSuccess(req.id)
})
}

View file

@ -0,0 +1,37 @@
module.exports = async (app, req, res) => {
if (req.method !== 'POST') return res.status(405).send("Method not allowed.")
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!")
let params = req.gdParams({
accountID: req.body.accountID,
gjp: app.xor.encrypt(req.body.password, 37526),
messageID: req.params.id,
})
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)
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
}
return res.send(msg)
})
}

View file

@ -0,0 +1,45 @@
module.exports = async (app, req, res) => {
if (req.method !== 'POST') return res.status(405).send("Method not allowed.")
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!")
let params = req.gdParams({
accountID: req.body.accountID,
gjp: app.xor.encrypt(req.body.password, 37526),
page: req.body.page || 0,
getSent: req.query.sent ? 1 : 0
})
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.`)
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
}
app.userCache(req.id, msg.accountID, msg.playerID, msg.author)
messageArray.push(msg)
})
return res.send(messageArray)
})
}

View file

@ -0,0 +1,26 @@
module.exports = async (app, req, res) => {
if (req.method !== 'POST') return res.status(405).send("Method not allowed.")
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!")
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 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,
})
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!')
app.trackSuccess(req.id)
})
}

39
api/post/like.js Normal file
View file

@ -0,0 +1,39 @@
const crypto = require('crypto')
function sha1(data) { return crypto.createHash("sha1").update(data, "binary").digest("hex"); }
module.exports = async (app, req, res) => {
if (req.method !== 'POST') return res.status(405).send("Method not allowed.")
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',
rs: '8f0l0ClAN1'
}
params.itemID = req.body.ID.toString()
params.gjp = app.xor.encrypt(req.body.password, 37526)
params.accountID = req.body.accountID.toString()
params.like = req.body.like.toString()
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"
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.`)
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)")
})
}

54
api/post/postComment.js Normal file
View file

@ -0,0 +1,54 @@
const crypto = require('crypto')
function sha1(data) { return crypto.createHash("sha1").update(data, "binary").digest("hex"); }
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
function getTime(time) {
let seconds = Math.ceil(time / 1000);
seconds = seconds % 60;
return seconds}
module.exports = async (app, req, res) => {
if (req.method !== 'POST') return res.status(405).send("Method not allowed.")
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.includes('\n')) return res.status(400).send("Comments cannot contain line breaks!")
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.gjp = app.xor.encrypt(req.body.password, 37526)
params.levelID = req.body.levelID.toString()
params.accountID = req.body.accountID.toString()
params.userName = req.body.username
let percent = parseInt(req.body.percent)
if (percent && percent > 0 && percent <= 100) params.percent = percent.toString()
let chk = params.userName + params.comment + params.levelID + params.percent + "0xPT6iUrtws0J"
chk = sha1(chk)
chk = app.xor.encrypt(chk, 29481)
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 (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"}`)
}
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);
})
}

View file

@ -0,0 +1,36 @@
const crypto = require('crypto')
function sha1(data) { return crypto.createHash("sha1").update(data, "binary").digest("hex"); }
module.exports = async (app, req, res) => {
if (req.method !== 'POST') return res.status(405).send("Method not allowed.")
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.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.gjp = app.xor.encrypt(req.body.password, 37526)
params.accountID = req.body.accountID.toString()
params.userName = req.body.username
let chk = params.userName + params.comment + "1xPT6iUrtws0J"
chk = sha1(chk)
chk = app.xor.encrypt(chk, 29481)
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.`)
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"}`)
}
else app.trackSuccess(req.id)
res.send(`Comment posted to ${params.userName} with ID ${body}`)
})
}

67
api/profile.js Normal file
View file

@ -0,0 +1,67 @@
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()
}
let username = getLevels || req.params.id
let probablyID
if (username.endsWith(".") && req.isGDPS) {
username = username.slice(0, -1)
probablyID = Number(username)
}
let accountMode = !req.query.hasOwnProperty("player") && Number(req.params.id)
let foundID = app.userCache(req.id, username)
let skipRequest = accountMode || foundID || probablyID
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) {
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 { // 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]
}
if (getLevels) {
req.params.text = foundID ? foundID[1] : app.parseResponse(b1)[2]
return app.run.search(app, req, res)
}
req.gdRequest('getGJUserInfo20', { targetAccountID: searchResult }, function (err2, res2, body) {
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 (!foundID) app.userCache(req.id, account[16], account[2], account[1])
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 variables = Object.keys(userData)
variables.forEach(x => {
let regex = new RegExp(`\\[\\[${x.toUpperCase()}\\]\\]`, "g")
html = html.replace(regex, app.clean(userData[x]))
})
return res.send(html)
})
})
})
}

162
api/search.js Normal file
View file

@ -0,0 +1,162 @@
const request = require('request')
const music = require('../misc/music.json')
const Level = require('../classes/Level.js')
let demonList = {}
module.exports = async (app, req, res) => {
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"
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) {
if (err1) return res.sendError()
else return request.get(req.server.demonList + 'api/v2/demons/listed/?limit=100&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()}
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 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,
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 (req.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)
}
if (req.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))) {
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.page = 0
}
if (req.isGDPS && filters.diff && !filters.len) filters.len = "-"
if (filters.str == "*") delete filters.str
req.gdRequest('getGJLevels21', req.gdParams(filters), function(err, resp, body) {
if (err) return res.sendError()
let splitBody = body.split('#')
let preRes = splitBody[0].split('|')
let authorList = {}
let songList = {}
let authors = splitBody[1].split('|')
let songs = splitBody[2]; songs = songs.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]]})
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";
if (demonMode) {
if (!y) level.demonList = req.server.demonList
level.demonPosition = demonList[req.id].list.indexOf(level.id) + 1
}
if (req.isGDPS) level.gdps = (req.onePointNine ? "1.9/" : "") + req.server.id
if (level.author != "-" && app.config.cacheAccountIDs) app.userCache(req.id, level.accountID, level.playerID, level.author)
//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(":");
if (filters.gauntlet) { // gauntlet page stuff
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))
}
else { // normal page stuff
level.results = +pages[0];
level.pages = +pages[0] == 9999 ? 1000 : +Math.ceil(pages[0] / amount);
}
}
parsedLevels[y] = level
})
if (filters.type == 10) parsedLevels = parsedLevels.slice((+filters.page) * amount, (+filters.page + 1) * amount)
return res.send(parsedLevels)
})
}

10
api/song.js Normal file
View file

@ -0,0 +1,10 @@
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)
})
}

BIN
assets/Pusab.ttf Normal file

Binary file not shown.

BIN
assets/achievements.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.3 KiB

BIN
assets/achievements/gd.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

BIN
assets/achievements/gdm.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 24 KiB

BIN
assets/achievements/gdr.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 40 KiB

BIN
assets/achievements/gds.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 31 KiB

BIN
assets/achievements/gdw.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 34 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 19 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

BIN
assets/arrow-left.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.5 KiB

BIN
assets/arrow-right.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.5 KiB

BIN
assets/back.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.7 KiB

BIN
assets/basement.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

BIN
assets/blankbutton.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 37 KiB

BIN
assets/bluebox.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

BIN
assets/bluecoin.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 7 KiB

BIN
assets/boomlings/Kaine.ttf Normal file

Binary file not shown.

BIN
assets/boomlings/blue.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

BIN
assets/boomlings/border.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 117 KiB

BIN
assets/boomlings/green.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 19 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 19 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

Some files were not shown because too many files have changed in this diff Show more