crystal-gauntlet/src/crystal-gauntlet.cr

329 lines
8.5 KiB
Crystal

require "http/server"
require "http/server/handler"
require "uri"
require "sqlite3"
require "migrate"
require "dotenv"
require "toml"
require "colorize"
require "option_parser"
require "migrate"
require "./enums"
require "./lib/hash"
require "./lib/format"
require "./lib/xor"
require "./lib/accounts"
require "./lib/gjp"
require "./lib/clean"
require "./lib/songs"
require "./lib/ids"
require "./lib/level"
require "./lib/dailies"
require "./lib/templates"
require "./lib/reupload"
require "./lib/creator_points"
require "./lib/versions"
require "./lib/ips"
require "./lib/ranks"
require "./lib/notifications"
require "./patch-exe.cr"
if File.exists?(".env")
Dotenv.load
end
include CrystalGauntlet::PatchExe
module CrystalGauntlet
VERSION = "0.1.0"
CONFIG = File.exists?("./config.toml") ? TOML.parse(File.read("./config.toml")) : TOML.parse("") # todo: log warning?
LOG = ::Log.for("crystal-gauntlet")
def config_get(key : String)
this = CONFIG
key.split(".").each do |val|
next_val = this.as(Hash)[val]?
if next_val == nil
return nil
else
this = next_val
end
end
return this
end
def config_get(key : String, default)
config_get(key).as?(typeof(default)) || default
end
DATABASE = DB.open(ENV["DATABASE_URL"]? || "sqlite3://./crystal-gauntlet.db")
# todo: unhardcore
DATA_FOLDER = Path.new("data")
@@endpoints = Hash(String, (HTTP::Server::Context -> String)).new
@@template_endpoints = Hash(String, (HTTP::Server::Context -> Nil)).new
@@up_at = nil
def self.uptime
if !@@up_at
return Time::Span::ZERO
else
return Time.utc - @@up_at.not_nil!
end
end
def self.uptime_s
span = uptime
Format.fmt_timespan_long(span)
end
def self.endpoints
@@endpoints
end
def self.template_endpoints
@@template_endpoints
end
def self.is_funny
config_get("general.easter_eggs", false)
end
def severity_color(severity : Log::Severity) : Colorize::Object
case severity
when .trace?
Colorize.with.dark_gray
when .debug?
Colorize.with.dark_gray
when .info?
Colorize.with.cyan
when .notice?
Colorize.with.cyan
when .warn?
Colorize.with.yellow
when .error?
Colorize.with.red
when .fatal?
Colorize.with.light_red
else
Colorize.with.white
end
end
struct CrystalGauntletFormat < Log::StaticFormatter
def run
Colorize.with.light_gray.dim.surround(@io) do
timestamp
end
string " "
severity_color(@entry.severity).surround(@io) do
@entry.severity.label.rjust(@io, 6)
end
string " "
Colorize.with.white.surround(@io) do
source
end
string " "
message
end
end
class GDHandler
include HTTP::Handler
def call(context : HTTP::Server::Context)
# expunge trailing slashes
path = context.request.path.chomp("/")
path = path.sub(config_get("general.append_path").as(String | Nil) || "", "")
body = context.request.body
if CrystalGauntlet.endpoints.has_key?(path) && context.request.method == "POST" && body
func = CrystalGauntlet.endpoints[path]
begin
value = func.call(context)
rescue err
LOG.error { "error while handling #{path.colorize(:white)}:" }
LOG.error { err.to_s }
is_relevant = true
err.backtrace.each do |str|
# this is a hack. Oh well
if str.starts_with?("src/crystal-gauntlet.cr") || (!is_relevant)
is_relevant = false
else
LOG.error {" #{str}"}
end
end
context.response.content_type = "text/plain"
context.response.respond_with_status(500, "-1")
else
max_size = 2048
value_displayed = value
if value.size > max_size
value_displayed = value[0..max_size] + ("".colorize(:dark_gray).to_s)
end
LOG.debug { "-> ".colorize(:green).to_s + value_displayed }
context.response.content_type = "text/plain"
# to let endpoints manually write to IO
if value != ""
context.response.print value
end
end
else
call_next(context)
end
end
end
class TemplateHandler
include HTTP::Handler
def call(context : HTTP::Server::Context)
# expunge trailing slashes
path = context.request.path.chomp("/")
body = context.request.body
if CrystalGauntlet.template_endpoints.has_key?(path)
func = CrystalGauntlet.template_endpoints[path]
begin
func.call(context)
rescue err
LOG.error { "error while handling #{path.colorize(:white)}:" }
LOG.error { err.to_s }
is_relevant = true
err.backtrace.each do |str|
# this is a hack. Oh well
if str.starts_with?("src/crystal-gauntlet.cr") || (!is_relevant)
is_relevant = false
else
LOG.error {" #{str}"}
end
end
context.response.content_type = "text/html"
context.response.respond_with_status(500, "Internal server error occurred, sorry about that")
end
else
call_next(context)
end
end
end
def self.run()
Log.setup_from_env(backend: Log::IOBackend.new(formatter: CrystalGauntletFormat))
migrate = false
calc_creator_points = false
patch_exe = false
patch_exe_location = nil
patch_exe_package = nil
parser = OptionParser.new do |parser|
parser.banner = "Usage: crystal-gauntlet [command] [arguments]"
parser.on("migrate", "Migrate the database") do
migrate = true
parser.banner = "Usage: crystal-gauntlet migrate [arguments]"
end
parser.on("calc_creator_points", "Calculate creator points and update them") do
calc_creator_points = true
parser.banner = "Usage: crystal-gauntlet calc_creator_points [arguments]"
end
parser.on("patch_exe", "Patch Geometry Dash executables with your server URL (supports #{SUPPORTED_PATCH_PLATFORMS.join(", ")})") do
patch_exe = true
parser.banner = "Usage: crystal-gauntlet patch_exe <file>"
parser.on("-p NAME", "--package=NAME", "Specify the new package name") { |name| patch_exe_package = name }
parser.unknown_args do |opt|
patch_exe_location = opt[0]?
end
end
parser.on("-h", "--help", "Show this help") do
puts parser
exit
end
end
parser.parse
if patch_exe
if !patch_exe_location
puts parser
exit 1
end
check_server_length(true)
LOG.info { "Patching #{patch_exe_location}" }
patch_exe_file(patch_exe_location.not_nil!, patch_exe_package)
exit
end
migrator = Migrate::Migrator.new(
DATABASE
)
if migrate
LOG.info { "Migrating #{ENV["DATABASE_URL"].colorize(:white)}..." }
migrator.to_latest
exit
end
if calc_creator_points
LOG.info { "updating creator points" }
DATABASE.query_all("select id, username, creator_points from users", as: {Int32, String, Int32}).each() do |id, username, old_count|
new_count = CreatorPoints.update_creator_points id
if old_count > 0 || new_count > 0
LOG.info { "#{username}: #{old_count} -> #{new_count}" }
end
end
exit
end
if !migrator.latest?
LOG.fatal { "Database hasn\'t been migrated!! Please run #{"crystal-gauntlet migrate".colorize(:white)}" }
exit 1
end
["songs", "levels", "saves"].each() { |v|
Dir.mkdir_p(DATA_FOLDER / v)
}
server = HTTP::Server.new([
HTTP::LogHandler.new,
HTTP::StaticFileHandler.new("public/", fallthrough: true, directory_listing: false),
HTTP::StaticFileHandler.new((DATA_FOLDER / "songs").to_s, fallthrough: true, directory_listing: false),
CrystalGauntlet::GDHandler.new,
CrystalGauntlet::TemplateHandler.new
])
listen_on = URI.parse(ENV["LISTEN_ON"]? || "http://localhost:8080").normalize
case listen_on.scheme
when "http"
server.bind_tcp(listen_on.hostname.not_nil!, listen_on.port.not_nil!)
when "unix"
server.bind_unix(listen_on.to_s.sub("unix://",""))
end
check_server_length(false)
Reupload.init()
Ranks.init()
@@up_at = Time.utc
LOG.notice { "Listening on #{listen_on.to_s.colorize(:white)}" }
server.listen
end
end
require "./endpoints/**"
require "./template_endpoints/**"
CrystalGauntlet.run()