added custom page for gauntlets
70
.github/workflows/analyse.yml
vendored
Normal 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
|
@ -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
|
@ -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
|
@ -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
|
@ -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
|
@ -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
|
@ -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
|
@ -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)
|
||||
|
||||
})
|
||||
|
||||
}
|
47
api/leaderboards/accurate.js
Normal 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)
|
||||
|
||||
})
|
||||
}
|
43
api/leaderboards/boomlings.js
Normal 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)
|
||||
|
||||
})
|
||||
}
|
53
api/leaderboards/leaderboardLevel.js
Normal 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))
|
||||
|
||||
})
|
||||
}
|
33
api/leaderboards/scores.js
Normal 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
|
@ -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
|
@ -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()
|
||||
}
|
23
api/messages/countMessages.js
Normal 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)
|
||||
})
|
||||
|
||||
}
|
24
api/messages/deleteMessage.js
Normal 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)
|
||||
})
|
||||
|
||||
}
|
37
api/messages/fetchMessage.js
Normal 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)
|
||||
})
|
||||
|
||||
}
|
45
api/messages/getMessages.js
Normal 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)
|
||||
})
|
||||
|
||||
}
|
26
api/messages/sendMessage.js
Normal 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
|
@ -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
|
@ -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);
|
||||
})
|
||||
}
|
36
api/post/postProfileComment.js
Normal 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
|
@ -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
|
@ -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
|
@ -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
BIN
assets/achievements.png
Normal file
After Width: | Height: | Size: 20 KiB |
BIN
assets/achievements/attempts.png
Normal file
After Width: | Height: | Size: 3.7 KiB |
BIN
assets/achievements/chamberOfTime.png
Normal file
After Width: | Height: | Size: 10 KiB |
BIN
assets/achievements/coins.png
Normal file
After Width: | Height: | Size: 8.4 KiB |
BIN
assets/achievements/color1_off.png
Normal file
After Width: | Height: | Size: 7.7 KiB |
BIN
assets/achievements/color1_on.png
Normal file
After Width: | Height: | Size: 13 KiB |
BIN
assets/achievements/color2_off.png
Normal file
After Width: | Height: | Size: 8.3 KiB |
BIN
assets/achievements/color2_on.png
Normal file
After Width: | Height: | Size: 13 KiB |
BIN
assets/achievements/creator.png
Normal file
After Width: | Height: | Size: 6.2 KiB |
BIN
assets/achievements/customLevels.png
Normal file
After Width: | Height: | Size: 10 KiB |
BIN
assets/achievements/demons.png
Normal file
After Width: | Height: | Size: 9.5 KiB |
BIN
assets/achievements/diamonds.png
Normal file
After Width: | Height: | Size: 6.3 KiB |
BIN
assets/achievements/gd.png
Normal file
After Width: | Height: | Size: 20 KiB |
BIN
assets/achievements/gdm.png
Normal file
After Width: | Height: | Size: 24 KiB |
BIN
assets/achievements/gdr.png
Normal file
After Width: | Height: | Size: 40 KiB |
BIN
assets/achievements/gds.png
Normal file
After Width: | Height: | Size: 31 KiB |
BIN
assets/achievements/gdw.png
Normal file
After Width: | Height: | Size: 34 KiB |
BIN
assets/achievements/jumps.png
Normal file
After Width: | Height: | Size: 6 KiB |
BIN
assets/achievements/level.png
Normal file
After Width: | Height: | Size: 8.7 KiB |
BIN
assets/achievements/mappacks.png
Normal file
After Width: | Height: | Size: 7.1 KiB |
BIN
assets/achievements/misc_off.png
Normal file
After Width: | Height: | Size: 12 KiB |
BIN
assets/achievements/misc_on.png
Normal file
After Width: | Height: | Size: 19 KiB |
BIN
assets/achievements/rating.png
Normal file
After Width: | Height: | Size: 6.5 KiB |
BIN
assets/achievements/robtop.png
Normal file
After Width: | Height: | Size: 10 KiB |
BIN
assets/achievements/secret.png
Normal file
After Width: | Height: | Size: 7.8 KiB |
BIN
assets/achievements/shardBonus.png
Normal file
After Width: | Height: | Size: 8.6 KiB |
BIN
assets/achievements/shardFire.png
Normal file
After Width: | Height: | Size: 6.2 KiB |
BIN
assets/achievements/shardIce.png
Normal file
After Width: | Height: | Size: 6.6 KiB |
BIN
assets/achievements/shardLava.png
Normal file
After Width: | Height: | Size: 6.5 KiB |
BIN
assets/achievements/shardPoison.png
Normal file
After Width: | Height: | Size: 7.3 KiB |
BIN
assets/achievements/shardShadow.png
Normal file
After Width: | Height: | Size: 7.2 KiB |
BIN
assets/achievements/social.png
Normal file
After Width: | Height: | Size: 7.7 KiB |
BIN
assets/achievements/stars.png
Normal file
After Width: | Height: | Size: 5.8 KiB |
BIN
assets/achievements/ultimate.png
Normal file
After Width: | Height: | Size: 10 KiB |
BIN
assets/achievements/usercoins.png
Normal file
After Width: | Height: | Size: 6.1 KiB |
BIN
assets/achievements/vaultOfSecrets.png
Normal file
After Width: | Height: | Size: 10 KiB |
BIN
assets/arrow-left.png
Normal file
After Width: | Height: | Size: 3.5 KiB |
BIN
assets/arrow-right.png
Normal file
After Width: | Height: | Size: 3.5 KiB |
BIN
assets/back.png
Normal file
After Width: | Height: | Size: 4.7 KiB |
BIN
assets/basement.png
Normal file
After Width: | Height: | Size: 20 KiB |
BIN
assets/blankbutton.png
Normal file
After Width: | Height: | Size: 37 KiB |
BIN
assets/bluebox.png
Normal file
After Width: | Height: | Size: 16 KiB |
BIN
assets/bluecoin.png
Normal file
After Width: | Height: | Size: 7 KiB |
BIN
assets/boomlings/Kaine.ttf
Normal file
BIN
assets/boomlings/blue.png
Normal file
After Width: | Height: | Size: 12 KiB |
BIN
assets/boomlings/border.png
Normal file
After Width: | Height: | Size: 117 KiB |
BIN
assets/boomlings/green.png
Normal file
After Width: | Height: | Size: 9.6 KiB |
BIN
assets/boomlings/icons/0.png
Normal file
After Width: | Height: | Size: 3.5 KiB |
BIN
assets/boomlings/icons/1.png
Normal file
After Width: | Height: | Size: 14 KiB |
BIN
assets/boomlings/icons/10.png
Normal file
After Width: | Height: | Size: 12 KiB |
BIN
assets/boomlings/icons/11.png
Normal file
After Width: | Height: | Size: 12 KiB |
BIN
assets/boomlings/icons/12.png
Normal file
After Width: | Height: | Size: 11 KiB |
BIN
assets/boomlings/icons/13.png
Normal file
After Width: | Height: | Size: 16 KiB |
BIN
assets/boomlings/icons/14.png
Normal file
After Width: | Height: | Size: 13 KiB |
BIN
assets/boomlings/icons/15.png
Normal file
After Width: | Height: | Size: 15 KiB |
BIN
assets/boomlings/icons/16.png
Normal file
After Width: | Height: | Size: 19 KiB |
BIN
assets/boomlings/icons/17.png
Normal file
After Width: | Height: | Size: 19 KiB |
BIN
assets/boomlings/icons/18.png
Normal file
After Width: | Height: | Size: 13 KiB |
BIN
assets/boomlings/icons/19.png
Normal file
After Width: | Height: | Size: 11 KiB |
BIN
assets/boomlings/icons/2.png
Normal file
After Width: | Height: | Size: 14 KiB |
BIN
assets/boomlings/icons/20.png
Normal file
After Width: | Height: | Size: 13 KiB |
BIN
assets/boomlings/icons/21.png
Normal file
After Width: | Height: | Size: 12 KiB |
BIN
assets/boomlings/icons/22.png
Normal file
After Width: | Height: | Size: 11 KiB |
BIN
assets/boomlings/icons/23.png
Normal file
After Width: | Height: | Size: 16 KiB |
BIN
assets/boomlings/icons/24.png
Normal file
After Width: | Height: | Size: 15 KiB |
BIN
assets/boomlings/icons/25.png
Normal file
After Width: | Height: | Size: 14 KiB |
BIN
assets/boomlings/icons/26.png
Normal file
After Width: | Height: | Size: 14 KiB |
BIN
assets/boomlings/icons/27.png
Normal file
After Width: | Height: | Size: 13 KiB |
BIN
assets/boomlings/icons/28.png
Normal file
After Width: | Height: | Size: 14 KiB |
BIN
assets/boomlings/icons/29.png
Normal file
After Width: | Height: | Size: 17 KiB |
BIN
assets/boomlings/icons/3.png
Normal file
After Width: | Height: | Size: 13 KiB |
BIN
assets/boomlings/icons/30.png
Normal file
After Width: | Height: | Size: 16 KiB |
BIN
assets/boomlings/icons/31.png
Normal file
After Width: | Height: | Size: 17 KiB |
BIN
assets/boomlings/icons/32.png
Normal file
After Width: | Height: | Size: 17 KiB |