some minor refactoring
This commit is contained in:
commit
4ca2ba06ab
|
@ -0,0 +1,9 @@
|
|||
root = true
|
||||
|
||||
[*.cr]
|
||||
charset = utf-8
|
||||
end_of_line = lf
|
||||
insert_final_newline = true
|
||||
indent_style = space
|
||||
indent_size = 2
|
||||
trim_trailing_whitespace = true
|
|
@ -0,0 +1 @@
|
|||
DATABASE_URL=sqlite3://./crystalgauntlet.db
|
|
@ -0,0 +1,6 @@
|
|||
/lib/
|
||||
/bin/
|
||||
/.shards/
|
||||
*.dwarf
|
||||
.env
|
||||
crystalgauntlet.db
|
|
@ -0,0 +1,6 @@
|
|||
{
|
||||
"crystal-lang.completion": true,
|
||||
"crystal-lang.hover": true,
|
||||
"crystal-lang.implementations": true,
|
||||
"crystal-lang.mainFile": "${workspaceRoot}/src/crystal-gauntlet.cr"
|
||||
}
|
|
@ -0,0 +1,16 @@
|
|||
# todo: move inside executable
|
||||
|
||||
require "log"
|
||||
require "dotenv"
|
||||
require "sqlite3"
|
||||
require "migrate"
|
||||
|
||||
Dotenv.load
|
||||
|
||||
desc "Migrate database to the latest version"
|
||||
task :dbmigrate do
|
||||
migrator = Migrate::Migrator.new(
|
||||
DB.open(ENV["DATABASE_URL"])
|
||||
)
|
||||
migrator.to_latest
|
||||
end
|
|
@ -0,0 +1,21 @@
|
|||
MIT License
|
||||
|
||||
Copyright (c) 2022 Jill "oatmealine" Monoids <oatmealine@disroot.org>
|
||||
|
||||
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.
|
|
@ -0,0 +1,23 @@
|
|||
# crystal-gauntlet
|
||||
|
||||
among balls
|
||||
|
||||
## build
|
||||
|
||||
`shards build`
|
||||
|
||||
you may need to head into `lib/` to fix deps. i'm Very sorry
|
||||
|
||||
## setup
|
||||
|
||||
copy `.env.example` to `.env` and fill it out
|
||||
|
||||
run `cake db:migrate` (must have [cake](https://github.com/axvm/cake/))
|
||||
|
||||
**schemas are highly unstable so you will be offered 0 support in migrating databases for now**, however in the future you'll want to run this each time you update
|
||||
|
||||
then `bin/crystal-gauntlet` (or `shards run`)
|
||||
|
||||
### real
|
||||
|
||||
![real](docs/crystal-gauntlet.jpg)
|
|
@ -0,0 +1,47 @@
|
|||
-- +migrate up
|
||||
CREATE TABLE levels (
|
||||
id SERIAL PRIMARY KEY,
|
||||
created_at TEXT NOT NULL DEFAULT (STRFTIME('%Y-%m-%d %H:%M:%f', 'now')),
|
||||
modified_at TEXT NOT NULL DEFAULT (STRFTIME('%Y-%m-%d %H:%M:%f', 'now')),
|
||||
|
||||
name TEXT NOT NULL,
|
||||
user_id INTEGER NOT NULL references users(id),
|
||||
description TEXT NOT NULL DEFAULT "",
|
||||
original INTEGER,
|
||||
|
||||
game_version INTEGER NOT NULL,
|
||||
binary_version INTEGER NOT NULL,
|
||||
|
||||
password TEXT,
|
||||
requested_stars INTEGER,
|
||||
unlisted INTEGER NOT NULL DEFAULT 0,
|
||||
|
||||
version INTEGER NOT NULL DEFAULT 0,
|
||||
level_data BLOB NOT NULL,
|
||||
extra_data BLOB NOT NULL,
|
||||
level_info BLOB NOT NULL,
|
||||
|
||||
-- checksums, presumably
|
||||
wt1 TEXT NOT NULL,
|
||||
wt2 TEXT NOT NULL,
|
||||
|
||||
song_id INTEGER NOT NULL,
|
||||
|
||||
length INTEGER NOT NULL,
|
||||
objects INTEGER NOT NULL,
|
||||
coins INTEGER NOT NULL DEFAULT 0,
|
||||
has_ldm INTEGER NOT NULL DEFAULT 0,
|
||||
two_player INTEGER NOT NULL DEFAULT 0,
|
||||
|
||||
downloads INTEGER NOT NULL DEFAULT 0,
|
||||
likes INTEGER NOT NULL DEFAULT 0,
|
||||
difficulty INTEGER,
|
||||
demon_difficulty INTEGER,
|
||||
stars INTEGER,
|
||||
featured INTEGER NOT NULL DEFAULT 0,
|
||||
epic INTEGER NOT NULL DEFAULT 0,
|
||||
rated_coins INTEGER NOT NULL DEFAULT 0
|
||||
);
|
||||
|
||||
-- +migrate down
|
||||
DROP TABLE levels;
|
|
@ -0,0 +1,49 @@
|
|||
-- +migrate up
|
||||
CREATE TABLE users (
|
||||
id SERIAL PRIMARY KEY,
|
||||
-- on a registered account, account_id refers to the
|
||||
-- account ID - however, pre 2.0, instead udid referred
|
||||
-- to the user's UUID or UDID, depending on platform.
|
||||
-- UUID and UDID are unique ids assigned for green
|
||||
-- username users
|
||||
--
|
||||
-- in short, if `registered`, use account_id, else, use udid
|
||||
udid TEXT,
|
||||
account_id INTEGER references accounts(id),
|
||||
registered INTEGER NOT NULL,
|
||||
|
||||
username TEXT NOT NULL,
|
||||
|
||||
stars INTEGER NOT NULL DEFAULT 0,
|
||||
demons INTEGER NOT NULL DEFAULT 0,
|
||||
coins INTEGER NOT NULL DEFAULT 0,
|
||||
user_coins INTEGER NOT NULL DEFAULT 0,
|
||||
diamonds INTEGER NOT NULL DEFAULT 0,
|
||||
orbs INTEGER NOT NULL DEFAULT 0,
|
||||
creator_points INTEGER NOT NULL DEFAULT 0,
|
||||
|
||||
completed_levels INTEGER NOT NULL DEFAULT 0,
|
||||
|
||||
icon_type INTEGER NOT NULL DEFAULT 0, -- icon to display in comments, etc
|
||||
color1 INTEGER NOT NULL DEFAULT 0,
|
||||
color2 INTEGER NOT NULL DEFAULT 3,
|
||||
cube INTEGER NOT NULL DEFAULT 0,
|
||||
ship INTEGER NOT NULL DEFAULT 0,
|
||||
ball INTEGER NOT NULL DEFAULT 0,
|
||||
ufo INTEGER NOT NULL DEFAULT 0,
|
||||
wave INTEGER NOT NULL DEFAULT 0,
|
||||
robot INTEGER NOT NULL DEFAULT 0,
|
||||
spider INTEGER NOT NULL DEFAULT 0,
|
||||
explosion INTEGER NOT NULL DEFAULT 0,
|
||||
special INTEGER NOT NULL DEFAULT 0,
|
||||
glow INTEGER NOT NULL DEFAULT 0,
|
||||
|
||||
created_at TEXT NOT NULL DEFAULT (STRFTIME('%Y-%m-%d %H:%M:%f', 'now')),
|
||||
last_played TEXT NOT NULL DEFAULT (STRFTIME('%Y-%m-%d %H:%M:%f', 'now')),
|
||||
|
||||
is_banned INTEGER NOT NULL DEFAULT 0,
|
||||
is_banned_upload INTEGER NOT NULL DEFAULT 0
|
||||
);
|
||||
|
||||
-- +migrate down
|
||||
DROP TABLE users;
|
|
@ -0,0 +1,24 @@
|
|||
-- +migrate up
|
||||
CREATE TABLE accounts (
|
||||
id SERIAL PRIMARY KEY,
|
||||
|
||||
username TEXT NOT NULL,
|
||||
password TEXT NOT NULL, -- bcrypt hashed
|
||||
gjp2 TEXT NOT NULL,
|
||||
email TEXT NOT NULL,
|
||||
|
||||
is_admin INTEGER NOT NULL DEFAULT 0,
|
||||
|
||||
messages_enabled INTEGER NOT NULL DEFAULT 1, -- messages from non-friends enabled
|
||||
friend_requests_enabled INTEGER NOT NULL DEFAULT 1, -- frs enabled
|
||||
comments_enabled INTEGER NOT NULL DEFAULT 0, -- able to see user's comments
|
||||
|
||||
youtube_url TEXT,
|
||||
twitter_url TEXT,
|
||||
twitch_url TEXT,
|
||||
|
||||
created_at TEXT NOT NULL DEFAULT (STRFTIME('%Y-%m-%d %H:%M:%f', 'now'))
|
||||
);
|
||||
|
||||
-- +migrate down
|
||||
DROP TABLE accounts;
|
Binary file not shown.
After Width: | Height: | Size: 72 KiB |
|
@ -0,0 +1,5 @@
|
|||
http://www.boomlings.com/database ->
|
||||
http://localhost:8080/asdfasdfasd
|
||||
|
||||
aHR0cDovL3d3dy5ib29tbGluZ3MuY29tL2RhdGFiYXNl -> base64 http://localhost:8080/asdfasdfasd
|
||||
aHR0cDovL2xvY2FsaG9zdDo4MDgwL2FzZGZhc2RmYXNk
|
|
@ -0,0 +1,22 @@
|
|||
version: 2.0
|
||||
shards:
|
||||
db:
|
||||
git: https://github.com/crystal-lang/crystal-db.git
|
||||
version: 0.6.0
|
||||
|
||||
dotenv:
|
||||
git: https://github.com/gdotdesign/cr-dotenv.git
|
||||
version: 0.1.0
|
||||
|
||||
migrate:
|
||||
git: https://github.com/vladfaust/migrate.cr.git
|
||||
version: 0.5.0
|
||||
|
||||
sqlite3:
|
||||
git: https://github.com/crystal-lang/crystal-sqlite3.git
|
||||
version: 0.13.0
|
||||
|
||||
time_format:
|
||||
git: https://github.com/vladfaust/time_format.cr.git
|
||||
version: 0.1.1
|
||||
|
|
@ -0,0 +1,23 @@
|
|||
name: crystal-gauntlet
|
||||
version: 0.1.0
|
||||
|
||||
authors:
|
||||
- Jill "oatmealine" Monoids <oatmealine@disroot.org>
|
||||
- winter <me@wint0r.zone>
|
||||
|
||||
targets:
|
||||
crystal-gauntlet:
|
||||
main: src/crystal-gauntlet.cr
|
||||
|
||||
dependencies:
|
||||
sqlite3:
|
||||
github: crystal-lang/crystal-sqlite3
|
||||
migrate:
|
||||
github: vladfaust/migrate.cr
|
||||
version: ~> 0.5.0
|
||||
dotenv:
|
||||
github: gdotdesign/cr-dotenv
|
||||
|
||||
crystal: 1.6.2
|
||||
|
||||
license: MIT
|
|
@ -0,0 +1,9 @@
|
|||
require "./spec_helper"
|
||||
|
||||
describe Crystal::Gauntlet do
|
||||
# TODO: Write tests
|
||||
|
||||
it "works" do
|
||||
false.should eq(true)
|
||||
end
|
||||
end
|
|
@ -0,0 +1,2 @@
|
|||
require "spec"
|
||||
require "../src/crystal-gauntlet"
|
|
@ -0,0 +1,31 @@
|
|||
require "uri"
|
||||
|
||||
include CrystalGauntlet
|
||||
|
||||
module CrystalGauntlet::Accounts
|
||||
extend self
|
||||
|
||||
def get_ext_id_from_params(params : URI::Params) : String
|
||||
return "1"
|
||||
if params.has_key?("udid") && params["udid"] != ""
|
||||
# todo: numeric id check
|
||||
params["udid"]
|
||||
elsif params.has_key?("account_id") && params["account_id"] != "" && params["account_id"] != "0"
|
||||
# todo: validate password
|
||||
params["account_id"]
|
||||
else
|
||||
"-1"
|
||||
end
|
||||
end
|
||||
|
||||
def get_user_id(username : String, ext_id : String) : Int32
|
||||
return 1
|
||||
DATABASE.query("select id from users where udid = ? or account_id = ?", ext_id, ext_id) do |rs|
|
||||
if rs.column_count > 0
|
||||
return rs.read(Int32)
|
||||
else
|
||||
raise "no user associated with account?!"
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
|
@ -0,0 +1,56 @@
|
|||
require "http/server"
|
||||
require "uri"
|
||||
require "sqlite3"
|
||||
require "migrate"
|
||||
require "dotenv"
|
||||
|
||||
require "./enums"
|
||||
require "./hash"
|
||||
require "./format"
|
||||
require "./accounts"
|
||||
require "./gjp"
|
||||
|
||||
Dotenv.load
|
||||
|
||||
module CrystalGauntlet
|
||||
VERSION = "0.1.0"
|
||||
|
||||
APPEND_PATH = "asdfasdfasd/"
|
||||
DATABASE = DB.open(ENV["DATABASE_URL"])
|
||||
|
||||
@@endpoints = Hash(String, (String -> String)).new
|
||||
|
||||
def self.endpoints
|
||||
@@endpoints
|
||||
end
|
||||
|
||||
def self.run()
|
||||
server = HTTP::Server.new do |context|
|
||||
# expunge trailing slashes
|
||||
path = context.request.path.chomp("/")
|
||||
|
||||
path = path.sub(APPEND_PATH, "")
|
||||
body = context.request.body
|
||||
|
||||
if !body
|
||||
puts "no body :("
|
||||
elsif @@endpoints.has_key?(path)
|
||||
func = @@endpoints[path]
|
||||
value = func.call(body.gets_to_end)
|
||||
context.response.content_type = "text/plain"
|
||||
context.response.print value
|
||||
puts "#{path} -> #{value}"
|
||||
else
|
||||
context.response.respond_with_status(404, "endpoint not found")
|
||||
puts "#{path} -> 404"
|
||||
end
|
||||
end
|
||||
|
||||
puts "Listening on http://127.0.0.1:8080"
|
||||
server.listen(8080)
|
||||
end
|
||||
end
|
||||
|
||||
require "./endpoints/**"
|
||||
|
||||
CrystalGauntlet.run()
|
|
@ -0,0 +1,27 @@
|
|||
require "uri"
|
||||
require "base64"
|
||||
require "crypto/bcrypt/password"
|
||||
|
||||
include CrystalGauntlet
|
||||
|
||||
CrystalGauntlet.endpoints["/accounts/loginGJAccount.php"] = ->(body : String): String {
|
||||
params = URI::Params.parse(body)
|
||||
puts params.inspect
|
||||
|
||||
username = params["userName"]
|
||||
password = params["password"]
|
||||
result = DATABASE.query_all("select id, password from accounts", as: {Int32, String})
|
||||
if result.size > 0
|
||||
account_id, hash = result[0]
|
||||
bcrypt = Crypto::Bcrypt::Password.new(hash)
|
||||
|
||||
if bcrypt.verify(password)
|
||||
user_id = Accounts.get_user_id(username, account_id.to_s)
|
||||
"#{account_id},#{user_id}"
|
||||
else
|
||||
return "-12"
|
||||
end
|
||||
else
|
||||
return "-1"
|
||||
end
|
||||
}
|
|
@ -0,0 +1,28 @@
|
|||
require "uri"
|
||||
require "base64"
|
||||
require "crypto/bcrypt/password"
|
||||
|
||||
include CrystalGauntlet
|
||||
|
||||
CrystalGauntlet.endpoints["/accounts/registerGJAccount.php"] = ->(body : String): String {
|
||||
params = URI::Params.parse(body)
|
||||
puts params.inspect
|
||||
|
||||
username = params["userName"]
|
||||
password = params["password"]
|
||||
email = params["email"]
|
||||
|
||||
username_exists = DATABASE.scalar "select count(*) from accounts where username = ?", username
|
||||
if username_exists != 0
|
||||
return "-2"
|
||||
end
|
||||
|
||||
password_hash = Crypto::Bcrypt::Password.create(password, cost: 10).to_s
|
||||
gjp2 = CrystalGauntlet::GJP.hash(password)
|
||||
next_id = (DATABASE.scalar("select max(id) from accounts").as(Int64 | Nil) || 0) + 1
|
||||
DATABASE.exec "insert into accounts (id, username, password, email, gjp2) values (?, ?, ?, ?, ?)", next_id, username, password_hash, email, gjp2
|
||||
|
||||
user_id = (DATABASE.scalar("select max(id) from users").as(Int64 | Nil) || 0) + 1
|
||||
DATABASE.exec "insert into users (id, account_id, username, registered) values (?, ?, ?, 1)", user_id, next_id, username
|
||||
"1"
|
||||
}
|
|
@ -0,0 +1,111 @@
|
|||
require "uri"
|
||||
require "base64"
|
||||
|
||||
include CrystalGauntlet
|
||||
|
||||
CrystalGauntlet.endpoints["/downloadGJLevel22.php"] = ->(body : String): String {
|
||||
params = URI::Params.parse(body)
|
||||
puts params.inspect
|
||||
|
||||
response = ""
|
||||
|
||||
DATABASE.query("select levels.id, levels.name, levels.level_data, levels.extra_data, levels.level_info, levels.password, levels.user_id, levels.description, levels.original, levels.game_version, levels.requested_stars, levels.version, levels.song_id, levels.length, levels.objects, levels.coins, levels.has_ldm, levels.two_player, levels.downloads, levels.likes, levels.difficulty, levels.demon_difficulty, levels.stars, levels.featured, levels.epic, levels.rated_coins, users.username, users.udid, users.account_id, users.registered from levels join users on levels.user_id = users.id where levels.id = ?", params["levelID"].to_i32) do |rs|
|
||||
if rs.move_next
|
||||
id = rs.read(Int32)
|
||||
name = rs.read(String)
|
||||
level_data = rs.read(String)
|
||||
extra_data = rs.read(String)
|
||||
level_info = rs.read(String)
|
||||
password = rs.read(String | Nil)
|
||||
user_id = rs.read(Int32)
|
||||
description = rs.read(String)
|
||||
original = rs.read(Int32 | Nil)
|
||||
game_version = rs.read(Int32)
|
||||
requested_stars = rs.read(Int32 | Nil)
|
||||
version = rs.read(Int32)
|
||||
song_id = rs.read(Int32)
|
||||
length = rs.read(Int32)
|
||||
objects = rs.read(Int32)
|
||||
coins = rs.read(Int32)
|
||||
has_ldm = rs.read(Bool)
|
||||
two_player = rs.read(Bool)
|
||||
downloads = rs.read(Int32)
|
||||
likes = rs.read(Int32)
|
||||
difficulty_int = rs.read(Int32 | Nil)
|
||||
difficulty = difficulty_int && LevelDifficulty.new(difficulty_int)
|
||||
demon_difficulty_int = rs.read(Int32 | Nil)
|
||||
demon_difficulty = demon_difficulty_int && DemonDifficulty.new(demon_difficulty_int)
|
||||
stars = rs.read(Int32 | Nil)
|
||||
featured = rs.read(Bool)
|
||||
epic = rs.read(Bool)
|
||||
rated_coins = rs.read(Bool)
|
||||
|
||||
user_username = rs.read(String)
|
||||
user_udid = rs.read(String | Nil)
|
||||
user_account_id = rs.read(Int32 | Nil)
|
||||
user_registered = rs.read(Bool)
|
||||
|
||||
xor_pass = "0"
|
||||
if !password
|
||||
password = "0"
|
||||
elsif params["gameVersion"].to_i >= 20
|
||||
xor_pass = GDBase64.encode(XorCrypt.encrypt_string(password, "26364"))
|
||||
else
|
||||
xor_pass = password
|
||||
end
|
||||
|
||||
# https://github.com/Cvolton/GMDprivateServer/blob/master/incl/levels/getGJLevels.php#L266
|
||||
response += CrystalGauntlet::Format.fmt_hash({
|
||||
1 => id,
|
||||
2 => name,
|
||||
3 => Base64.encode(description).sub('/', '_').sub('+', '-').strip("\n"),
|
||||
4 => level_data,
|
||||
5 => version,
|
||||
6 => user_id,
|
||||
8 => 10,
|
||||
9 => difficulty ? difficulty.to_star_difficulty : 0, # 0=N/A 10=EASY 20=NORMAL 30=HARD 40=HARDER 50=INSANE 50=AUTO 50=DEMON
|
||||
10 => downloads,
|
||||
11 => 1,
|
||||
12 => song_id < 50 ? song_id : 0,
|
||||
13 => game_version,
|
||||
14 => likes,
|
||||
17 => difficulty && difficulty.demon?,
|
||||
43 => (demon_difficulty || DemonDifficulty::Hard).to_demon_difficulty,
|
||||
25 => difficulty && difficulty.auto?,
|
||||
18 => stars || 0,
|
||||
19 => featured,
|
||||
42 => epic,
|
||||
45 => objects,
|
||||
15 => length,
|
||||
30 => original || 0,
|
||||
31 => two_player,
|
||||
28 => "1",
|
||||
29 => "1",
|
||||
35 => song_id >= 50 ? song_id : 0,
|
||||
36 => extra_data,
|
||||
37 => coins,
|
||||
38 => rated_coins,
|
||||
39 => requested_stars || 0,
|
||||
46 => 1,
|
||||
47 => 2,
|
||||
40 => has_ldm,
|
||||
27 => xor_pass,
|
||||
# 0 for n/a, 10 for easy, 20, for medium, ...
|
||||
})
|
||||
|
||||
if params.has_key?("extras")
|
||||
response += ":26:" + level_info
|
||||
end
|
||||
|
||||
response += "#" + Hashes.gen_solo(level_data)
|
||||
|
||||
thing = [user_id, stars || 0, difficulty && difficulty.demon?, id, rated_coins, featured, password, 0].map! { |x| Format.fmt_value(x) }
|
||||
puts thing.join(",")
|
||||
response += "#" + Hashes.gen_solo_2(thing.join(","))
|
||||
else
|
||||
response += "-1"
|
||||
end
|
||||
end
|
||||
|
||||
response
|
||||
}
|
|
@ -0,0 +1,93 @@
|
|||
require "uri"
|
||||
require "base64"
|
||||
|
||||
include CrystalGauntlet
|
||||
|
||||
CrystalGauntlet.endpoints["/getGJLevels21.php"] = ->(body : String): String {
|
||||
params = URI::Params.parse(body)
|
||||
puts params.inspect
|
||||
|
||||
results = [] of String
|
||||
users = [] of String
|
||||
songs = [] of String
|
||||
|
||||
hash_data = [] of Tuple(Int32, Int32, Bool)
|
||||
|
||||
DATABASE.query "select levels.id, levels.name, levels.user_id, levels.description, levels.original, levels.game_version, levels.requested_stars, levels.version, levels.song_id, levels.length, levels.objects, levels.coins, levels.has_ldm, levels.two_player, levels.downloads, levels.likes, levels.difficulty, levels.demon_difficulty, levels.stars, levels.featured, levels.epic, levels.rated_coins, users.username, users.udid, users.account_id, users.registered from levels join users on levels.user_id = users.id" do |rs|
|
||||
rs.each do
|
||||
id = rs.read(Int32)
|
||||
name = rs.read(String)
|
||||
user_id = rs.read(Int32)
|
||||
description = rs.read(String)
|
||||
original = rs.read(Int32 | Nil)
|
||||
game_version = rs.read(Int32)
|
||||
requested_stars = rs.read(Int32 | Nil)
|
||||
version = rs.read(Int32)
|
||||
song_id = rs.read(Int32)
|
||||
length = rs.read(Int32)
|
||||
objects = rs.read(Int32)
|
||||
coins = rs.read(Int32)
|
||||
has_ldm = rs.read(Bool)
|
||||
two_player = rs.read(Bool)
|
||||
downloads = rs.read(Int32)
|
||||
likes = rs.read(Int32)
|
||||
difficulty_int = rs.read(Int32 | Nil)
|
||||
difficulty = difficulty_int && LevelDifficulty.new(difficulty_int)
|
||||
demon_difficulty_int = rs.read(Int32 | Nil)
|
||||
demon_difficulty = demon_difficulty_int && DemonDifficulty.new(demon_difficulty_int)
|
||||
stars = rs.read(Int32 | Nil)
|
||||
featured = rs.read(Bool)
|
||||
epic = rs.read(Bool)
|
||||
rated_coins = rs.read(Bool)
|
||||
|
||||
user_username = rs.read(String)
|
||||
user_udid = rs.read(String | Nil)
|
||||
user_account_id = rs.read(Int32 | Nil)
|
||||
user_registered = rs.read(Bool)
|
||||
|
||||
# https://github.com/Cvolton/GMDprivateServer/blob/master/incl/levels/getGJLevels.php#L266
|
||||
results << CrystalGauntlet::Format.fmt_hash({
|
||||
1 => id,
|
||||
2 => name,
|
||||
5 => version,
|
||||
6 => user_id,
|
||||
8 => 10,
|
||||
9 => difficulty ? difficulty.to_star_difficulty : 0, # 0=N/A 10=EASY 20=NORMAL 30=HARD 40=HARDER 50=INSANE 50=AUTO 50=DEMON
|
||||
10 => downloads,
|
||||
12 => song_id < 50 ? song_id : 0,
|
||||
13 => game_version,
|
||||
14 => likes,
|
||||
17 => difficulty && difficulty.demon?,
|
||||
43 => (demon_difficulty || DemonDifficulty::Hard).to_demon_difficulty,
|
||||
25 => difficulty && difficulty.auto?,
|
||||
18 => stars || 0,
|
||||
19 => featured,
|
||||
42 => epic,
|
||||
45 => objects,
|
||||
3 => Base64.encode(description).sub('/', '_').sub('+', '-').strip("\n"),
|
||||
15 => length,
|
||||
30 => original || 0,
|
||||
31 => two_player,
|
||||
37 => coins,
|
||||
38 => rated_coins,
|
||||
39 => requested_stars || 0,
|
||||
46 => 1,
|
||||
47 => 2,
|
||||
40 => has_ldm,
|
||||
35 => song_id >= 50 ? song_id : 0, # 0 for n/a, 10 for easy, 20, for medium, ...
|
||||
})
|
||||
|
||||
users << "#{user_id}:#{user_username}:#{user_registered ? user_account_id : user_udid}"
|
||||
|
||||
hash_data << {id, stars || 0, rated_coins}
|
||||
end
|
||||
end
|
||||
|
||||
# `:${offset}:${levelsPerPage}`
|
||||
searchMeta = "#{results.size}:0:10"
|
||||
|
||||
res = [results.join("|"), users.join("|"), songs.join("|"), searchMeta, CrystalGauntlet::Hashes.gen_multi(hash_data)].join("#")
|
||||
puts res
|
||||
|
||||
res
|
||||
}
|
|
@ -0,0 +1,35 @@
|
|||
require "uri"
|
||||
|
||||
include CrystalGauntlet
|
||||
|
||||
# URI::Params{"gameVersion" => ["21"], "binaryVersion" => ["35"], "gdw" => ["0"], "accountID" => ["8369"], "gjp" => ["Vw9mW0FBUgN_VXtZ"], "userName" => ["oatmealine"], "levelID" => ["0"], "levelName" => ["security"], "levelDesc" => [""], "levelVersion" => ["1"], "levelLength" => ["1"], "audioTrack" => ["0"], "auto" => ["0"], "password" => ["0"], "original" => ["0"], "twoPlayer" => ["0"], "songID" => ["1050575"], "objects" => ["207"], "coins" => ["0"], "requestedStars" => ["0"], "unlisted" => ["0"], "wt" => ["709"], "wt2" => ["0"], "ldm" => ["0"], "extraString" => ["0_73_0_39_0_0_0_0_0_0_0_0_66_0_0_0_0_0_0_0_0_0_0_0_0_0_0_0_0_0_0_0_0_0_0_0_0_0_0_0_0_0_0_0_0_0_0_0_0_0_0_0_0_0_0"], "seed" => ["SbpPMJ9vjn"], "seed2" => ["UFIKAAQCA1RTB1cABFEMUlBRDwFRUgMPAlAHVFYMUQFXAwEGCAAPDQ=="], "levelString" => ["H4sIAAAAAAAAC61Xy5HYIAxtyLuDJCTB5JQatgAKSAspPvxssC1lc8jBZngPhNAP-PVF6YASpWBhKlQkFoCCzAVwNNSbWD6gSIEQQtECBbj9UgklFfgNtcX6Uf2-mQ7udAhYxuhvRGRTRBszJvyTkLrfYusyBAE2Qe3_jSD-X4LEEXT8-gl0hNbwaGQ08ah_OaB1dECzSa35otx72P9DQid-xv4fbJ3dGzjCDzhAYzzwoHDQAbyA3IDYgdoD4qtL1HiQhmj91dU-ucIHNDbCTmIl8MB46JjZydxICHOq9rldlUAXLXlMCBVB7BMApjLViLtumDpdl8QmI8SuRlhMTEe1WRpLILbdxqnCpXGkKaQh0BCBU-xLzS7j4iuEXfM1oyr8IW3bH7TPPjcg-_Ktr6dFth0MkYNRWDuoqAZjfMPJwcWTPyXt8gdOb7zvWofzaNipxcnYdO5AMzoE2UyJHUm6mWpCp602u5xTL8NAyPaOANAj2COSQyB4RPQIfahJjkNqAHuE5ZJOGDvGsK_ysFnEhzLRs0D0LMCWBYa_gZGW-JGg3LefX8EEHF_ELAdh1IOKpFE88i2FZx-2RUScRYQ8IryIUT9A-WGiCWzLaXKkKjpEAo94W2ESL7uR9IIXljFmCRxbOdXN-a5_zSHbkxgc32MwfI8hbNQ9rBCcrEBwsgLBqmK9fIeH-uhkBaKTFYhGVkTdhMsqzw0lz0DkGYic5MDoGSJahuj-w1gLZ6Uwr_MUuSuKtUbK8ZEvTQc8CypRn86yYRuQ-R7cfbA88v8EZAHpyr4ecKh6P0D3PvZz8xlwacvXljwXpNeQjGPI00yZHTy98Sl6iFKTYp9Kb6rfbMBUgEJ0cH3jYWPydSNYi89FLL3mOjaltsoQbNX2a1jvizMue7adok1thnSbEp_K9h7QjgdCOx4I3_EgEpakRNtVM2wKoBstcy2bcqKFnGghJ1rIiJa5BPkxQX5MkBMTlLNTbirVyk3aLmtTVjTc1nE0ZI17sUGcwhxHRydYIzm4E7TRD9roR2Y04nmtsmroFN9i3BBinTu3bd85yuMVgfrZSOFP5A2OaMJsj1Z7dLqPPtVh6wA7OefUI07G3teMty_YSVJ2i_acYvqI_QxlJw3nVgyN2XcjG2f4NCcH08oMpk-Y7NHRHi326DxggNjef_sDjsS5VJA4tysy34hL1NtV4hQs8QuW-AVLjKp0Uu9aNi0gYdgr3Qxwkoh_IelvM8V0gyTTDZIfidRAdWqWOjXLe3GT9-Qm883dCetJO02pfp1T_9xW_3DWd82eZlEwraV2SVO7pKld0tQuafosaUv5daVRzU8P1IOvEmm-2Wo0jNszmijRwHv9qGrsGBtY2rBxmibQ_TT9A3vViWgxFQAA"], "levelInfo" => ["H4sIAAAAAAAACyXQyxHAIAgE0I6YAPJx7L-v7MIl5ImA-snRp--T0w_f8EFcIs8gB7WZGvSiY9CD68Stx35P5WM1QhM2y-JBzESEImaiSvI_d1cZcZkw-dDMN-Mz3Xegy0XNke8CR0wJ60EYUTro816yUhEuthV7KoIGMUcr8SQiBolMb-sWw8UUz8DeiqugH0LOuW2zJoXVH1ldIOdMAQAA"], "secret" => ["Wmfd2893gb7"]}
|
||||
|
||||
CrystalGauntlet.endpoints["/uploadGJLevel21.php"] = ->(body : String): String {
|
||||
params = URI::Params.parse(body)
|
||||
puts params.inspect
|
||||
|
||||
ext_id = Accounts.get_ext_id_from_params(params)
|
||||
if ext_id == "-1"
|
||||
return "-1"
|
||||
end
|
||||
user_id = Accounts.get_user_id(params["userName"], ext_id)
|
||||
|
||||
song_id = params["songID"] == "0" ? params["audioTrack"] : params["songID"]
|
||||
|
||||
description = params["levelDesc"]
|
||||
if params["gameVersion"].to_i >= 20 # 2.0
|
||||
description = GDBase64.decode description
|
||||
end
|
||||
|
||||
if DATABASE.scalar("select count(*) from levels where id = ? and user_id = ?", params["levelID"], params["accountID"]).as(Int64) > 0
|
||||
# update existing level
|
||||
raise "not implemented"
|
||||
else
|
||||
# create new level
|
||||
next_id = (DATABASE.scalar("select max(id) from levels").as(Int64 | Nil) || 0) + 1
|
||||
|
||||
DATABASE.exec("insert into levels (id, name, user_id, description, original, game_version, binary_version, password, requested_stars, unlisted, version, level_data, extra_data, level_info, wt1, wt2, song_id, length, objects, coins, has_ldm, two_player) values (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)", next_id, params["levelName"], user_id, description, params["original"].to_i32, params["gameVersion"].to_i32, params["binaryVersion"].to_i32, params["password"] == "0" ? nil : params["password"].to_i32, params["requestedStars"].to_i32, params["unlisted"].to_i32, params["levelVersion"].to_i32, params["levelString"], params["extraString"], params["levelInfo"], params["wt"], params["wt2"], song_id.to_i32, params["levelLength"].to_i32, params["objects"].to_i32, params["coins"].to_i32, params["ldm"].to_i32, params["twoPlayer"].to_i32)
|
||||
|
||||
next_id.to_s
|
||||
end
|
||||
}
|
|
@ -0,0 +1,89 @@
|
|||
require "uri"
|
||||
|
||||
include CrystalGauntlet
|
||||
|
||||
# URI::Params{"gameVersion" => ["21"], "binaryVersion" => ["35"], "gdw" => ["0"], "accountID" => ["1"], "gjp" => ["XFZBX1NSW1xcUw=="], "targetAccountID" => ["1"], "secret" => ["Wmfd2893gb7"]}
|
||||
|
||||
CrystalGauntlet.endpoints["/getGJUserInfo20.php"] = ->(body : String): String {
|
||||
params = URI::Params.parse(body)
|
||||
puts params.inspect
|
||||
|
||||
DATABASE.query("select accounts.id, accounts.username, is_admin, messages_enabled, friend_requests_enabled, comments_enabled, youtube_url, twitter_url, twitch_url, accounts.created_at, users.id, stars, demons, coins, user_coins, diamonds, orbs, creator_points, icon_type, color1, color2, glow, cube, ship, ball, ufo, wave, robot, spider, explosion from accounts join users on accounts.id = users.account_id where accounts.id = ?", params["targetAccountID"]) do |rs|
|
||||
if rs.move_next
|
||||
id = rs.read(Int32)
|
||||
username = rs.read(String)
|
||||
is_admin = rs.read(Int32)
|
||||
messages_enabled = rs.read(Int32)
|
||||
friend_requests_enabled = rs.read(Int32)
|
||||
comments_enabled = rs.read(Int32)
|
||||
youtube_url = rs.read(String | Nil)
|
||||
twitter_url = rs.read(String | Nil)
|
||||
twitch_url = rs.read(String | Nil)
|
||||
created_at = rs.read(String)
|
||||
user_id = rs.read(Int32)
|
||||
stars = rs.read(Int32)
|
||||
demons = rs.read(Int32)
|
||||
coins = rs.read(Int32)
|
||||
user_coins = rs.read(Int32)
|
||||
diamonds = rs.read(Int32)
|
||||
orbs = rs.read(Int32)
|
||||
creator_points = rs.read(Int32)
|
||||
icon_type = rs.read(Int32)
|
||||
color1 = rs.read(Int32)
|
||||
color2 = rs.read(Int32)
|
||||
glow = rs.read(Int32)
|
||||
cube = rs.read(Int32)
|
||||
ship = rs.read(Int32)
|
||||
ball = rs.read(Int32)
|
||||
ufo = rs.read(Int32)
|
||||
wave = rs.read(Int32)
|
||||
robot = rs.read(Int32)
|
||||
spider = rs.read(Int32)
|
||||
explosion = rs.read(Int32)
|
||||
|
||||
return CrystalGauntlet::Format.fmt_hash({
|
||||
1 => username,
|
||||
2 => user_id,
|
||||
13 => coins,
|
||||
17 => user_coins,
|
||||
10 => color1,
|
||||
11 => color2,
|
||||
3 => stars,
|
||||
46 => diamonds,
|
||||
4 => demons,
|
||||
8 => creator_points,
|
||||
18 => !messages_enabled,
|
||||
19 => !friend_requests_enabled,
|
||||
50 => !comments_enabled,
|
||||
20 => youtube_url || "",
|
||||
21 => cube,
|
||||
22 => ship,
|
||||
23 => ball,
|
||||
24 => ufo,
|
||||
25 => wave,
|
||||
26 => robot,
|
||||
28 => glow,
|
||||
43 => spider,
|
||||
47 => explosion,
|
||||
30 => 1, # rank; todo
|
||||
16 => id,
|
||||
# 31 = isnt (0) or is (1) friend or (3) incoming request or (4) outgoing request
|
||||
# todo
|
||||
31 => 0,
|
||||
# also w/ friend requests:
|
||||
# 32 => id,
|
||||
# 35 => comment,
|
||||
# 37 => date,
|
||||
44 => twitter_url || "",
|
||||
45 => twitch_url || "",
|
||||
29 => 1,
|
||||
# badge, todo
|
||||
49 => 0
|
||||
})
|
||||
end
|
||||
end
|
||||
|
||||
"-1"
|
||||
|
||||
# echo "1:".$user["userName"].":2:".$user["userID"].":13:".$user["coins"].":17:".$user["userCoins"].":10:".$user["color1"].":11:".$user["color2"].":3:".$user["stars"].":46:".$user["diamonds"].":4:".$user["demons"].":8:".$creatorpoints.":18:".$msgstate.":19:".$reqsstate.":50:".$commentstate.":20:".$accinfo["youtubeurl"].":21:".$user["accIcon"].":22:".$user["accShip"].":23:".$user["accBall"].":24:".$user["accBird"].":25:".$user["accDart"].":26:".$user["accRobot"].":28:".$user["accGlow"].":43:".$user["accSpider"].":47:".$user["accExplosion"].":30:".$rank.":16:".$user["extID"].":31:".$friendstate.":44:".$accinfo["twitter"].":45:".$accinfo["twitch"].":29:1:49:".$badge . $appendix;
|
||||
}
|
|
@ -0,0 +1,17 @@
|
|||
require "uri"
|
||||
|
||||
include CrystalGauntlet
|
||||
|
||||
# URI::Params{"gameVersion" => ["21"], "binaryVersion" => ["35"], "gdw" => ["0"], "accountID" => ["1"], "gjp" => ["XFZBX1NSW1xcUw=="], "targetAccountID" => ["1"], "secret" => ["Wmfd2893gb7"]}
|
||||
|
||||
CrystalGauntlet.endpoints["/updateGJUserScore22.php"] = ->(body : String): String {
|
||||
params = URI::Params.parse(body)
|
||||
puts params.inspect
|
||||
|
||||
account_id = Accounts.get_ext_id_from_params(params)
|
||||
user_id = Accounts.get_user_id(params["userName"], account_id)
|
||||
|
||||
DATABASE.exec("update users set username=?, stars=?, demons=?, coins=?, user_coins=?, diamonds=?, icon_type=?, color1=?, color2=?, cube=?, ship=?, ball=?, ufo=?, wave=?, robot=?, spider=?, explosion=?, special=?, glow=?, last_played=? where id=?", params["userName"], params["stars"], params["demons"], params["coins"], params["userCoins"], params["diamonds"], params["iconType"], params["color1"], params["color2"], params["accIcon"], params["accShip"], params["accBall"], params["accBird"], params["accDart"], params["accRobot"], params["accSpider"], params["accExplosion"], params["special"], params["accGlow"], Time.utc.to_s("%Y-%m-%d %H:%M:%S"), user_id)
|
||||
|
||||
user_id.to_s
|
||||
}
|
|
@ -0,0 +1,63 @@
|
|||
module CrystalGauntlet
|
||||
enum LevelLength
|
||||
Tiny
|
||||
Short
|
||||
Medium
|
||||
Long
|
||||
XL
|
||||
end
|
||||
|
||||
enum LevelDifficulty
|
||||
Auto
|
||||
Easy
|
||||
Normal
|
||||
Hard
|
||||
Harder
|
||||
Insane
|
||||
Demon
|
||||
|
||||
def to_star_difficulty
|
||||
case self
|
||||
when .auto?
|
||||
50
|
||||
when .easy?
|
||||
10
|
||||
when .normal?
|
||||
20
|
||||
when .hard?
|
||||
30
|
||||
when .harder?
|
||||
40
|
||||
when .insane?
|
||||
50
|
||||
when .demon?
|
||||
50
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
enum DemonDifficulty
|
||||
Easy
|
||||
Medium
|
||||
Hard
|
||||
Insane
|
||||
Extreme
|
||||
# unsafe
|
||||
#Tarsorado
|
||||
|
||||
def to_demon_difficulty
|
||||
case self
|
||||
when .easy?
|
||||
3
|
||||
when .medium?
|
||||
4
|
||||
when .hard?
|
||||
0
|
||||
when .insane?
|
||||
5
|
||||
when .extreme?
|
||||
6
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
|
@ -0,0 +1,50 @@
|
|||
module CrystalGauntlet::Format
|
||||
extend self
|
||||
|
||||
def fmt_value(v) : String
|
||||
case v
|
||||
when Bool
|
||||
v ? "1" : "0"
|
||||
when String
|
||||
v
|
||||
else
|
||||
v.to_s
|
||||
end
|
||||
end
|
||||
|
||||
def fmt_hash(hash) : String
|
||||
hash.map_with_index{ |(i, v)| "#{i}:#{fmt_value(v)}" }.join(":")
|
||||
end
|
||||
end
|
||||
|
||||
module CrystalGauntlet::GDBase64
|
||||
extend self
|
||||
|
||||
def encode(v)
|
||||
Base64.encode(v).sub('/', '_').sub('+', '-')
|
||||
end
|
||||
|
||||
def decode(v)
|
||||
Base64.decode_string(v.sub('_', '/').sub('-', '+'))
|
||||
end
|
||||
end
|
||||
|
||||
module CrystalGauntlet::XorCrypt
|
||||
extend self
|
||||
|
||||
def encrypt(x : Bytes, key : Bytes) : Bytes
|
||||
result = Bytes.new(x.size)
|
||||
x.each.with_index() do |chr, index|
|
||||
result[index] = (chr ^ key[index % key.size])
|
||||
end
|
||||
result
|
||||
end
|
||||
|
||||
def encrypt_string(x : String, key : String) : Bytes
|
||||
result = Bytes.new(x.bytesize)
|
||||
x.bytes.each.with_index() do |chr, index|
|
||||
result[index] = (chr ^ key.byte_at(index % key.bytesize))
|
||||
end
|
||||
result
|
||||
end
|
||||
end
|
|
@ -0,0 +1,34 @@
|
|||
require "crypto/bcrypt/password"
|
||||
require "base64"
|
||||
|
||||
module CrystalGauntlet::GJP
|
||||
extend self
|
||||
|
||||
XOR_KEY = "37526"
|
||||
|
||||
def decrypt(pass : String)
|
||||
pwd = Base64.decode_string(pass.sub('_', '/').sub('-', '+'))
|
||||
decrypted = ""
|
||||
|
||||
pwd.each.with_index() do |chr, index|
|
||||
decrypted += (chr ^ XOR_KEY.byte_at(index % XOR_KEY.bytesize)).unsafe_chr
|
||||
end
|
||||
|
||||
decrypted
|
||||
end
|
||||
|
||||
def encrypt(pass : String)
|
||||
encrypted = Bytes.new(pass.bytesize)
|
||||
|
||||
pass.bytes.each.with_index() do |chr, index|
|
||||
encrypted[index] = chr ^ XOR_KEY.byte_at(index % XOR_KEY.bytesize)
|
||||
end
|
||||
|
||||
Base64.encode(encrypted).sub('/', '_').sub('+', '-')
|
||||
end
|
||||
|
||||
def hash(pass : String)
|
||||
gjp2_hash = Digest::SHA1.hexdigest(pass + "mI29fmAnxgTs")
|
||||
Crypto::Bcrypt::Password.create(gjp2_hash, cost: 10).to_s
|
||||
end
|
||||
end
|
|
@ -0,0 +1,56 @@
|
|||
require "digest/sha1"
|
||||
require "crypto/bcrypt"
|
||||
|
||||
module CrystalGauntlet::Hashes
|
||||
extend self
|
||||
|
||||
def gen_multi(level_hash_data : Array(Tuple(Int32, Int32, Bool)))
|
||||
Digest::SHA1.hexdigest do |ctx|
|
||||
level_hash_data.each.with_index() do |val, index|
|
||||
level_id, stars, coins = val
|
||||
level_id_str = level_id.to_s
|
||||
ctx.update "#{level_id_str[0]}#{level_id_str[-1]}#{stars}#{coins ? 1 : 0}"
|
||||
end
|
||||
|
||||
ctx.update "xI25fpAapCQg"
|
||||
end
|
||||
end
|
||||
|
||||
def gen_solo(level_string : String) : String
|
||||
hash = ""
|
||||
divided : Int32 = (level_string.size / 40).to_i
|
||||
i = 0
|
||||
k : Int32 = 0
|
||||
while k < level_string.size
|
||||
if i > 39
|
||||
break
|
||||
end
|
||||
|
||||
hash += level_string.char_at(k)
|
||||
i += 1
|
||||
k += divided
|
||||
end
|
||||
Digest::SHA1.hexdigest(hash.ljust(5, 'a') + "xI25fpAapCQg")
|
||||
end
|
||||
|
||||
def gen_solo_2(level_multi_string : String) : String
|
||||
Digest::SHA1.hexdigest do |ctx|
|
||||
ctx.update level_multi_string
|
||||
ctx.update "xI25fpAapCQg"
|
||||
end
|
||||
end
|
||||
|
||||
def gen_solo_3(level_multi_string : String) : String
|
||||
Digest::SHA1.hexdigest do |ctx|
|
||||
ctx.update level_multi_string
|
||||
ctx.update "oC36fpYaPtdg"
|
||||
end
|
||||
end
|
||||
|
||||
def gen_solo_4(level_multi_string : String) : String
|
||||
Digest::SHA1.hexdigest do |ctx|
|
||||
ctx.update level_multi_string
|
||||
ctx.update "pC26fpYaQCtg"
|
||||
end
|
||||
end
|
||||
end
|
Loading…
Reference in New Issue