Compare commits

...

4 Commits

Author SHA1 Message Date
Jill 344edf369c
comments & level page polish 2023-05-26 11:21:04 +03:00
Jill dc6d5767bb
individual level pages!!!!!!!!! 2023-05-24 11:27:44 +03:00
Jill 543a959829
migrate to athena for web routing 2023-05-23 11:46:31 +03:00
Jill 7e2dbd2917
update flake 2023-05-23 10:42:17 +03:00
24 changed files with 587 additions and 197 deletions

View File

@ -3,16 +3,16 @@
"ameba-src": {
"flake": false,
"locked": {
"lastModified": 1668496515,
"narHash": "sha256-SZ2sBQeZgtPOYioH9eK5MveFtWVGPvgKMrqsCfjoRGM=",
"lastModified": 1679041484,
"narHash": "sha256-pc9mtVR/PBhM5l1PnDkm+y+McxbrfAmQzxmLi761VF4=",
"owner": "crystal-ameba",
"repo": "ameba",
"rev": "cc687d028180203cb53390484871ffb8669b88c8",
"rev": "7c74d196d6d9a496a81a0c7b79ef44f39faf41b8",
"type": "github"
},
"original": {
"owner": "crystal-ameba",
"ref": "v1.3.1",
"ref": "v1.4.3",
"repo": "ameba",
"type": "github"
}
@ -37,13 +37,13 @@
"crystal-aarch64-darwin": {
"flake": false,
"locked": {
"narHash": "sha256-d5znjl8QaQ/dMNa4GtKS/K14PwjBgt8Md6AwSCKcyl4=",
"narHash": "sha256-NqYaZHM3kHAgYbO0RDJtA8eHqp4vVe4MBpisTOGrRVw=",
"type": "tarball",
"url": "https://github.com/crystal-lang/crystal/releases/download/1.6.1/crystal-1.6.1-1-darwin-universal.tar.gz"
"url": "https://github.com/crystal-lang/crystal/releases/download/1.8.1/crystal-1.8.1-1-darwin-universal.tar.gz"
},
"original": {
"type": "tarball",
"url": "https://github.com/crystal-lang/crystal/releases/download/1.6.1/crystal-1.6.1-1-darwin-universal.tar.gz"
"url": "https://github.com/crystal-lang/crystal/releases/download/1.8.1/crystal-1.8.1-1-darwin-universal.tar.gz"
}
},
"crystal-flake": {
@ -51,7 +51,6 @@
"ameba-src": "ameba-src",
"bdwgc-src": "bdwgc-src",
"crystal-aarch64-darwin": "crystal-aarch64-darwin",
"crystal-i686-linux": "crystal-i686-linux",
"crystal-src": "crystal-src",
"crystal-x86_64-darwin": "crystal-x86_64-darwin",
"crystal-x86_64-linux": "crystal-x86_64-linux",
@ -60,11 +59,11 @@
"nixpkgs": "nixpkgs"
},
"locked": {
"lastModified": 1672548746,
"narHash": "sha256-CUpInKhwsX/jQvqeyGPC7mHQV6VWjaCOEwE+fh1uPwI=",
"lastModified": 1683429373,
"narHash": "sha256-Mx5lwMyk2T40wFqOoYcJLs4srwO2UrsepTZhlHNuTrI=",
"owner": "manveru",
"repo": "crystal-flake",
"rev": "322876b36ae530b7648eb2692b9fa2c0f4e068b7",
"rev": "e7a443c20e2be6e5dd870586705dd27c91aa9c5c",
"type": "github"
},
"original": {
@ -73,31 +72,19 @@
"type": "github"
}
},
"crystal-i686-linux": {
"flake": false,
"locked": {
"narHash": "sha256-Hfs70OcJYh+HlGQEftthr1qj10yRCAjzlsqnlUrLdjg=",
"type": "tarball",
"url": "https://github.com/crystal-lang/crystal/releases/download/1.6.2/crystal-1.6.2-1-linux-x86_64.tar.gz"
},
"original": {
"type": "tarball",
"url": "https://github.com/crystal-lang/crystal/releases/download/1.6.2/crystal-1.6.2-1-linux-x86_64.tar.gz"
}
},
"crystal-src": {
"flake": false,
"locked": {
"lastModified": 1667495929,
"narHash": "sha256-WgU6Y8ww1IYyB0vd5tXwmWBEL5RiPjHA7YzPd21jlsY=",
"lastModified": 1681995387,
"narHash": "sha256-t+1vM1m62UftCvfa90Dg6nqt6Zseh/GP/Gc1VfOa4+c=",
"owner": "crystal-lang",
"repo": "crystal",
"rev": "879691b2e3268ab290a2a0951bd1d6032f0d90f3",
"rev": "a59a3dbd738269d5aad6051c3834fc70f482f469",
"type": "github"
},
"original": {
"owner": "crystal-lang",
"ref": "1.6.2",
"ref": "1.8.1",
"repo": "crystal",
"type": "github"
}
@ -105,40 +92,40 @@
"crystal-x86_64-darwin": {
"flake": false,
"locked": {
"narHash": "sha256-Hfs70OcJYh+HlGQEftthr1qj10yRCAjzlsqnlUrLdjg=",
"narHash": "sha256-NqYaZHM3kHAgYbO0RDJtA8eHqp4vVe4MBpisTOGrRVw=",
"type": "tarball",
"url": "https://github.com/crystal-lang/crystal/releases/download/1.6.2/crystal-1.6.2-1-linux-x86_64.tar.gz"
"url": "https://github.com/crystal-lang/crystal/releases/download/1.8.1/crystal-1.8.1-1-darwin-universal.tar.gz"
},
"original": {
"type": "tarball",
"url": "https://github.com/crystal-lang/crystal/releases/download/1.6.2/crystal-1.6.2-1-linux-x86_64.tar.gz"
"url": "https://github.com/crystal-lang/crystal/releases/download/1.8.1/crystal-1.8.1-1-darwin-universal.tar.gz"
}
},
"crystal-x86_64-linux": {
"flake": false,
"locked": {
"narHash": "sha256-Hfs70OcJYh+HlGQEftthr1qj10yRCAjzlsqnlUrLdjg=",
"narHash": "sha256-/Jk3uiglM/hzjygxmMUgVTvz+tuFFjBv8+uUIL05rXo=",
"type": "tarball",
"url": "https://github.com/crystal-lang/crystal/releases/download/1.6.2/crystal-1.6.2-1-linux-x86_64.tar.gz"
"url": "https://github.com/crystal-lang/crystal/releases/download/1.8.1/crystal-1.8.1-1-linux-x86_64.tar.gz"
},
"original": {
"type": "tarball",
"url": "https://github.com/crystal-lang/crystal/releases/download/1.6.2/crystal-1.6.2-1-linux-x86_64.tar.gz"
"url": "https://github.com/crystal-lang/crystal/releases/download/1.8.1/crystal-1.8.1-1-linux-x86_64.tar.gz"
}
},
"crystalline-src": {
"flake": false,
"locked": {
"lastModified": 1665222943,
"narHash": "sha256-j/rQf+qn5ZjZhq3wG4WEKzG3YwRcnUEb4y36qXpiW7Q=",
"lastModified": 1681549124,
"narHash": "sha256-kx3rdGqIbrOaHY7V3uXLqIFEYzzsMKzNwZ6Neq8zM3c=",
"owner": "elbywan",
"repo": "crystalline",
"rev": "41665a798659459ecd6e2cb1169d7faf1783e746",
"rev": "4ac0ae282c5f4172230fea1e93df51c2b380f475",
"type": "github"
},
"original": {
"owner": "elbywan",
"ref": "v0.7.0",
"ref": "v0.9.0",
"repo": "crystalline",
"type": "github"
}
@ -162,12 +149,15 @@
}
},
"flake-utils": {
"inputs": {
"systems": "systems"
},
"locked": {
"lastModified": 1667395993,
"narHash": "sha256-nuEHfE/LcWyuSWnS8t12N1wc105Qtau+/OdUAjtQ0rA=",
"lastModified": 1681202837,
"narHash": "sha256-H+Rh19JDwRtpVPAWp64F+rlEtxUWBAQW28eAi3SRSzg=",
"owner": "numtide",
"repo": "flake-utils",
"rev": "5aed5285a952e0b949eb3ba02c12fa4fcfef535f",
"rev": "cfacdce06f30d2b68473a46042957675eebb3401",
"type": "github"
},
"original": {
@ -178,16 +168,16 @@
},
"nixpkgs": {
"locked": {
"lastModified": 1658406394,
"narHash": "sha256-hgibXbbmxucpVJy9eOXKn7HxQtVkpeZ8euSnWl6c9Mk=",
"lastModified": 1677543769,
"narHash": "sha256-LwbqS8vGisXl2WHpK9r5+kodr0zoIT8F2YB0R4y1TsA=",
"owner": "NixOS",
"repo": "nixpkgs",
"rev": "c93e5ab157b45adbb6165bd85a9d8f67e49ff31d",
"rev": "b26d52c9feb6476580016e78935cbf96eb3e2115",
"type": "github"
},
"original": {
"owner": "NixOS",
"ref": "nixos-22.05",
"ref": "nixos-22.11",
"repo": "nixpkgs",
"type": "github"
}
@ -212,11 +202,11 @@
},
"nixpkgs_2": {
"locked": {
"lastModified": 1672501226,
"narHash": "sha256-jzR7tUl9VZU77RpXKjG8V7VT9PVoaK8FmJnb+eNTm3Q=",
"lastModified": 1682600000,
"narHash": "sha256-ha4BehR1dh8EnXSoE1m/wyyYVvHI9txjW4w5/oxsW5Y=",
"owner": "NixOS",
"repo": "nixpkgs",
"rev": "5353b10bd9a05533a94cf2e5629cf18b0af089dc",
"rev": "50fc86b75d2744e1ab3837ef74b53f103a9b55a0",
"type": "github"
},
"original": {
@ -232,6 +222,21 @@
"flake-utils": "flake-utils",
"nixpkgs": "nixpkgs_2"
}
},
"systems": {
"locked": {
"lastModified": 1681028828,
"narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=",
"owner": "nix-systems",
"repo": "default",
"rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e",
"type": "github"
},
"original": {
"owner": "nix-systems",
"repo": "default",
"type": "github"
}
}
},
"root": "root",

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.7 KiB

View File

@ -128,7 +128,7 @@
</div>
</div>
</div>
<a href="/accounts/notifications/" title="Notifiations" class="circle-button notifications <%= unread_notifications ? "notifications-unread" : "" %>">
<a href="/accounts/notifications" title="Notifiations" class="circle-button notifications <%= unread_notifications ? "notifications-unread" : "" %>">
<%= File.read("public/assets/icons/bell.svg") %>
</a>
</div>

View File

@ -38,7 +38,7 @@
<li>The <a href="https://git.oat.zone/oat/crystal-gauntlet">Git repository</a></li>
<li><a href="/accounts">Account stuff</a></li>
<li><a href="/tools/song_upload">Song reuploading</a> and <a href="/tools/song_search">searching</a></li>
<li><a href="/tools/levels">Levels</a> and <a href="/tools/reupload">level reuploading</a></li>
<li><a href="/levels">Levels</a> and <a href="/tools/reupload">level reuploading</a></li>
<%- if config_get("sessions.allow").as(Bool | Nil) -%>
<li>The <a href="/tools/create_session">session creation page</a> (for accessing features in 1.9)</li>
<%- end -%>

316
public/template/level.ecr Normal file
View File

@ -0,0 +1,316 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/png" href="/favicon.png" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<link rel="stylesheet" href="/style.css" />
<title><%= name %> by <%= username %></title>
<style>
body {
max-width: 800px;
margin: auto;
padding: 1em;
}
.card {
background-color: var(--background-color-2);
border-radius: 1.5em;
padding: 1em;
display: flex;
flex-direction: row;
gap: 1rem;
margin-bottom: 1em;
}
.card.bright {
background-color: var(--accent-color);
color: #000;
}
.card.bright ::selection {
background-color: #000;
color: #fff;
}
.card.bright a {
color: #000;
}
.card-l {
flex: 0 0 auto;
display: flex;
flex-direction: column;
align-items: center;
gap: 0.5em;
}
.level-img {
display: block;
height: 3rem;
}
.stars {
font-size: 1.1em;
font-weight: bold;
}
.stars img {
width: auto;
height: 1em;
vertical-align: middle;
}
.card-r {
display: flex;
flex-direction: column;
flex: 1 1 auto;
}
.card-top {
display: flex;
}
.label-left {
flex: 1 1 auto;
font-size: 1.5em;
font-weight: bold;
}
.label-right {
flex: 0 0 auto;
}
.label-right img {
height: 1em;
width: auto;
vertical-align: middle;
}
.card-header {
font-weight: bold;
color: var(--text-color-dark);
text-transform: uppercase;
font-size: 0.8em;
user-select: none;
}
.favicon {
margin: auto;
display: block;
}
.description {
flex-direction: column;
gap: 0.5em;
}
.leaderboard {
min-width: 100%;
overflow: auto;
font-size: 0.9em;
border-collapse: separate;
border-spacing: 0;
}
/* honestly quite horrendous */
.leaderboard tr:nth-child(2) td:first-child {
border-top-left-radius: 0.5em;
}
.leaderboard tr:nth-child(2) td:last-child {
border-top-right-radius: 0.5em;
}
.leaderboard tr:last-child td:first-child {
border-bottom-left-radius: 0.5em;
}
.leaderboard tr:last-child td:last-child {
border-bottom-right-radius: 0.5em;
}
.leaderboard tr:not(.leaderboard-header):nth-child(even) td {
background-color: var(--background-color-2);
}
.leaderboard th, .leaderboard td {
padding: 0.2em 0.5em;
text-align: left;
}
.leaderboard th {
font-size: 0.8em;
user-select: none;
color: var(--text-color-dark);
text-transform: uppercase;
}
.leaderboard td.rank {
width: 30px;
font-weight: bold;
}
.leaderboard td.percent {
width: 75px;
}
.leaderboard td.coins {
width: 4em;
}
.leaderboard td.coins .coin-container {
display: flex;
flex-direction: row;
}
.leaderboard td.coins .coin-container img {
width: 1em;
height: auto;
}
.leaderboard td.icon {
width: 2.5em;
text-align: center;
}
.leaderboard td.name {
width: 180px
}
.leaderboard td.name a {
text-decoration: none;
}
.leaderboard .player-icon {
height: 1.2em;
width: auto;
vertical-align: middle;
}
.comment {
flex-direction: column;
}
.comment-author {
display: flex;
flex-direction: row;
gap: 1ex;
align-items: center;
}
.comment-author .player-icon {
height: 1.5em;
width: auto;
}
.comment-date {
color: var(--text-color-dark);
}
.comment-author-label {
font-weight: bold;
text-decoration: none;
}
</style>
</head>
<body>
<a href="/"><img src="/favicon.png" width="64" height="auto" class="spinny favicon"></a><br>
<div class="card bright">
<div class="card-l">
<%=
difficulty = (difficulty_set || difficulty_community).try { |n| LevelDifficulty.new(n) }
demon_difficulty = demon_difficulty_int.try { |n| DemonDifficulty.new(n) }
"<img src='#{Templates.get_difficulty_icon(difficulty, featured, epic, demon_difficulty)}' class='level-img'>"
%>
<%- if stars -%>
<div class="stars">
<%= stars %> <img src="/assets/icons/gd/star.png">
</div>
<%- end -%>
</div>
<div class="card-r">
<div class="card-top">
<div class="label-left">
<%= name %>
</div>
<div class="label-right">
<%= downloads %> <img src="/assets/icons/gd/download.png"> <%= likes %> <img src="/assets/icons/gd/like.png">
</div>
</div>
<div class="card-bottom">
by <a href="/user/<%= username %>"><%= username %></a>
</div>
</div>
</div>
<div class="card description">
<div class="card-header">DESCRIPTION</div>
<div>
<%= description != "" ? HTML.escape(description) : "<i>No description provided</i>" %>
</div>
</div>
<div class="card description">
<div class="card-header">SONG</div>
<div>
<%- if song_name -%>
<!--
song_name, song_author, song_url, song_author_url
-->
<div><a href="<%= song_url %>" target="_blank" rel="noopener"><%= song_name %></a></div>
<%- if song_author && song_author != "" -%>
<div>by <a href="<%= song_author_url %>" target="_blank" rel="noopener"><%= song_author %></a></div>
<%- else -%>
<div><i>unknown artist</i></div>
<%- end -%>
<%- else -%>
idk
<%- end -%>
</div>
</div>
<table class="leaderboard">
<tr class="leaderboard-header">
<th class="rank">RANK</td>
<th class="percent">PERCENT</td>
<th class="coins">COINS</td>
<th class="icon"></td>
<th class="name">PLAYER</td>
<th class="date">TIME</td>
</tr>
<%
rank = 0
scores.each do |percent, coins, username, icon_type, color1, color2, cube, ship, ball, ufo, wave, robot, spider, special, set_at|
rank = rank + 1
icon_value = [cube, ship, ball, ufo, wave, robot, spider][icon_type]
type_str = ["cube", "ship", "ball", "ufo", "wave", "robot", "spider"][icon_type]
set_at_date = Time.parse(set_at, Format::TIME_FORMAT, Time::Location::UTC)
%>
<tr>
<td class="rank">#<%= rank %></td>
<td class="percent"><%= percent %>%</td>
<td class="coins">
<div class="coin-container">
<%- coins.times do |i| -%>
<img src="/assets/icons/gd/<%= rated_coins ? "silvercoin" : "browncoin" %>.png">
<%- end -%>
</div>
</td>
<td class="icon">
<img src="https://gdicon.oat.zone/icon.png?type=<%=type_str%>&value=<%=icon_value%>&color1=<%=color1%>&color2=<%=color2%><%=special ? "&glow=1" : ""%>" class="player-icon">
</td>
<td class="name">
<a href="/user/<%= username %>"><%= username %></a>
</td>
<td class="date">
<time datetime="<%= Time::Format::RFC_3339.format(set_at_date) %>" title="<%= Time::Format::RFC_2822.format(set_at_date) %>"><%= Format.fmt_timespan(Time.utc - set_at_date) %></time>
</td>
</tr>
<%- end -%>
<%- if scores.size == 0 -%>
<tr>
<td colspan="6" style="text-align: center">
<i>No scores</i>
</td>
</tr>
<%- end -%>
</table>
<h3>Comments</h3>
<%- comments.each do |comment, created_at, username, icon_type, color1, color2, cube, ship, ball, ufo, wave, robot, spider, special|
icon_value = [cube, ship, ball, ufo, wave, robot, spider][icon_type]
type_str = ["cube", "ship", "ball", "ufo", "wave", "robot", "spider"][icon_type]
created_at_date = Time.parse(created_at, Format::TIME_FORMAT, Time::Location::UTC)
%>
<div class="card comment">
<div class="comment-author">
<img src="https://gdicon.oat.zone/icon.png?type=<%=type_str%>&value=<%=icon_value%>&color1=<%=color1%>&color2=<%=color2%><%=special ? "&glow=1" : ""%>" class="player-icon">
<a class="comment-author-label" href="/user/<%= username %>"><%= username %></a>
<time class="comment-date" datetime="<%= Time::Format::RFC_3339.format(created_at_date) %>" title="<%= Time::Format::RFC_2822.format(created_at_date) %>"><%= Format.fmt_timespan(Time.utc - created_at_date) %> ago</time>
</div>
<div>
<%= HTML.escape(comment) %>
</div>
</div>
<%- end -%>
</body>
</html>

View File

@ -55,7 +55,10 @@
display: flex;
flex-direction: column;
align-items: center;
gap: 1em
gap: 1em;
}
.level a {
text-decoration: none;
}
</style>
</head>
@ -69,8 +72,8 @@
"<img src='#{Templates.get_difficulty_icon(difficulty, featured, epic, demon_difficulty)}' class='level-img'>"
%>
<div class="level-right">
<span class="line"><span class="name"><%= name %></span><span class="id dim">#<%= id %></span></span>
<small>by <%= username %></small><br>
<span class="line"><span class="name"><a href="/levels/<%= id %>"><%= name %></a></span><span class="id dim">#<%= id %></span></span>
<small>by <a href="/user/<%= username %>"><%= username %></a></small><br>
</div>
</div>
<%- end -%>

View File

@ -1,5 +1,9 @@
version: 2.0
shards:
athena-routing:
git: https://github.com/athena-framework/routing.git
version: 0.1.6
db:
git: https://github.com/crystal-lang/crystal-db.git
version: 0.11.0

View File

@ -23,6 +23,9 @@ dependencies:
branch: master
http-session:
github: straight-shoota/http-session
athena-routing:
github: athena-framework/routing
version: ~> 0.1.0
crystal: 1.6.2

View File

@ -9,6 +9,7 @@ require "colorize"
require "option_parser"
require "migrate"
require "./server"
require "./enums"
require "./lib/hash"
require "./lib/format"
@ -64,7 +65,10 @@ module CrystalGauntlet
DATA_FOLDER = Path.new("data")
@@endpoints = Hash(String, (HTTP::Server::Context -> String)).new
@@template_endpoints = Hash(String, (HTTP::Server::Context -> Nil)).new
@@template_endpoints = Hash(
NamedTuple(name: String, path: String, methods: Enumerable(String)),
Proc(HTTP::Server::Context, Hash(String, String?), Nil)
).new
@@up_at = nil
@ -132,90 +136,6 @@ module CrystalGauntlet
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))
@ -294,31 +214,7 @@ module CrystalGauntlet
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
Server.run()
end
end

View File

@ -64,18 +64,16 @@ CrystalGauntlet.endpoints["/uploadGJLevel21.php"] = ->(context : HTTP::Server::C
LOG.debug { "allowed objects: #{allowed_objects.inspect}" }
if forbidden_obj = level_objects.find do |obj|
if !obj.has_key?("1")
false
end
id = obj["1"].to_i
if allowed_objects.size > 0
if !allowed_objects.includes?(id)
true
end
else
if forbidden_objects.includes?(id)
true
if obj.has_key?("1")
id = obj["1"].to_i
if allowed_objects.size > 0
if !allowed_objects.includes?(id)
true
end
else
if forbidden_objects.includes?(id)
true
end
end
end
end
@ -83,12 +81,12 @@ CrystalGauntlet.endpoints["/uploadGJLevel21.php"] = ->(context : HTTP::Server::C
return "-1"
end
if exploit_obj = level_objects.find do |obj|
if exploit_obj = level_objects.find { |obj|
# target color ID
(obj.has_key?("23") && obj["23"].to_i < 0 || obj["23"].to_i > 1100) ||
(obj.has_key?("23") && (obj["23"].to_i < 0 || obj["23"].to_i > 1100)) ||
# target group ID
(obj.has_key?("51") && obj["51"].to_i < 0 || obj["51"].to_i > 1100)
end
(obj.has_key?("51") && (obj["51"].to_i < 0 || obj["51"].to_i > 1100))
}
LOG.info { "preventing upload of level attempting to exploit invalid color/group IDs" }
return "-1"
end
@ -147,7 +145,7 @@ CrystalGauntlet.endpoints["/uploadGJLevel21.php"] = ->(context : HTTP::Server::C
# create new level
next_id = IDs.get_next_id("levels")
DATABASE.exec("insert into levels (id, name, user_id, description, original, game_version, binary_version, password, requested_stars, unlisted, version, extra_data, level_info, editor_time, editor_time_copies, song_id, length, objects, coins, has_ldm, two_player) values (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)", next_id, Clean.clean_special(params["levelName"])[..20-1], user_id, description[..140-1], params["original"].to_i, params["gameVersion"].to_i, (params["binaryVersion"]? || "0").to_i, params["password"] == "0" ? nil : params["password"].to_i, requested_stars, (params["unlisted"]? || "0").to_i == 1, params["levelVersion"].to_i, Clean.clean_special(extraString), Clean.clean_b64(params["levelInfo"]? || ""), (params["wt"]? || "0").to_i, (params["wt2"]? || "0").to_i, song_id.to_i, level_length, objects, coins, (params["ldm"]? || "0").to_i == 1, two_player)
DATABASE.exec("insert into levels (id, name, user_id, description, original, game_version, binary_version, password, requested_stars, unlisted, version, extra_data, level_info, editor_time, editor_time_copies, song_id, length, objects, coins, has_ldm, two_player) values (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)", next_id, Clean.clean_basic(params["levelName"])[..20-1], user_id, description[..140-1], params["original"].to_i, params["gameVersion"].to_i, (params["binaryVersion"]? || "0").to_i, params["password"] == "0" ? nil : params["password"].to_i, requested_stars, (params["unlisted"]? || "0").to_i == 1, params["levelVersion"].to_i, Clean.clean_special(extraString), Clean.clean_b64(params["levelInfo"]? || ""), (params["wt"]? || "0").to_i, (params["wt2"]? || "0").to_i, song_id.to_i, level_length, objects, coins, (params["ldm"]? || "0").to_i == 1, two_player)
File.write(DATA_FOLDER / "levels" / "#{next_id.to_s}.lvl", Base64.decode(params["levelString"]))

93
src/server.cr Normal file
View File

@ -0,0 +1,93 @@
require "athena-routing"
module CrystalGauntlet::Server
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
def self.run()
template_handler = ART::RoutingHandler.new
CrystalGauntlet.template_endpoints.each do |key, handler|
template_handler.add(
key[:name],
ART::Route.new(
key[:path],
methods: key[:methods]
)
) { |ctx, params| handler.call(ctx, params) }
end
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),
GDHandler.new,
template_handler.compile
])
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

View File

@ -1,11 +1,19 @@
include CrystalGauntlet
CrystalGauntlet.template_endpoints["/#{config_get("general.append_path").as(String | Nil) || ""}accounts/accountManagement.php"] = ->(context : HTTP::Server::Context) {
CrystalGauntlet.template_endpoints[{
name: "account_management_redirect",
path: "/#{config_get("general.append_path").as(String | Nil) || ""}accounts/accountManagement.php",
methods: ["get"]
}] = ->(context : HTTP::Server::Context, params : Hash(String, String?)) {
context.response.headers.add("Location", "/accounts/")
context.response.status = HTTP::Status::MOVED_PERMANENTLY
}
CrystalGauntlet.template_endpoints["/accounts"] = ->(context : HTTP::Server::Context) {
CrystalGauntlet.template_endpoints[{
name: "account_management",
path: "/accounts",
methods: ["get"]
}] = ->(context : HTTP::Server::Context, params : Hash(String, String?)) {
context.response.content_type = "text/html"
account_id = nil

View File

@ -2,7 +2,12 @@ require "uri"
include CrystalGauntlet
CrystalGauntlet.template_endpoints["/accounts/settings"] = ->(context : HTTP::Server::Context) {
CrystalGauntlet.template_endpoints[{
name: "account_settings",
path: "/accounts/settings",
methods: ["get", "post"]
}] = ->(context : HTTP::Server::Context, params : Hash(String, String?)) {
context.response.content_type = "text/html"
account_id = nil

View File

@ -3,7 +3,11 @@ require "compress/gzip"
include CrystalGauntlet
CrystalGauntlet.template_endpoints["/tools/create_session"] = ->(context : HTTP::Server::Context) {
CrystalGauntlet.template_endpoints[{
name: "create_session",
path: "/tools/create_session",
methods: ["get", "post"]
}] = ->(context : HTTP::Server::Context, params : Hash(String, String?)) {
disabled = !config_get("sessions.allow").as(Bool | Nil)
result = nil
body = context.request.body

View File

@ -2,12 +2,21 @@ require "ecr"
include CrystalGauntlet
CrystalGauntlet.template_endpoints["/tools"] = ->(context : HTTP::Server::Context) {
CrystalGauntlet.template_endpoints[{
name: "tools_redirect",
path: "/tools",
methods: ["get"]
}] = ->(context : HTTP::Server::Context, params : Hash(String, String?)) {
context.response.headers.add("Location", "/")
context.response.status = HTTP::Status::TEMPORARY_REDIRECT
}
CrystalGauntlet.template_endpoints[""] = ->(context : HTTP::Server::Context) {
CrystalGauntlet.template_endpoints[{
name: "index",
path: "/",
methods: ["get"]
}] = ->(context : HTTP::Server::Context, params : Hash(String, String?)) {
context.response.content_type = "text/html"
ECR.embed("./public/template/index.ecr", context.response)
}

View File

@ -4,10 +4,37 @@ include CrystalGauntlet
levels_per_page = 10
CrystalGauntlet.template_endpoints["/tools/levels"] = ->(context : HTTP::Server::Context) {
CrystalGauntlet.template_endpoints[{
name: "list_levels",
path: "/levels",
methods: ["get"]
}] = ->(context : HTTP::Server::Context, params : Hash(String, String?)) {
context.response.content_type = "text/html"
page = (context.request.query_params["page"]? || "0").to_i? || 0
total_levels = DATABASE.scalar("select count(*) from levels").as(Int64)
levels = DATABASE.query_all("select levels.id, name, users.username, levels.community_difficulty, levels.difficulty, levels.demon_difficulty, levels.featured, levels.epic from levels left join users on levels.user_id = users.id order by levels.id desc limit #{levels_per_page} offset #{page * levels_per_page}", as: {Int32, String, String, Int32?, Int32?, Int32?, Bool, Bool})
ECR.embed("./public/template/levels.ecr", context.response)
}
CrystalGauntlet.template_endpoints[{
name: "level_page",
path: "/levels/{id<\\d+>}",
methods: ["get"]
}] = ->(context : HTTP::Server::Context, params : Hash(String, String?)) {
context.response.content_type = "text/html"
id = params["id"].as(String).to_i
begin
name, username, difficulty_community, difficulty_set, demon_difficulty_int, featured, epic, rated_coins, downloads, likes, stars, description, song_id, song_name, song_author, song_url, song_author_url = DATABASE.query_one("select levels.name, users.username, community_difficulty, difficulty, demon_difficulty, featured, epic, rated_coins, downloads, likes, levels.stars, description, song_id, song_data.name, song_authors.name, songs.url, song_authors.source from levels left join users on levels.user_id = users.id left join songs on songs.id = levels.song_id left join song_data on song_data.id = levels.song_id left join song_authors on song_data.author_id = song_authors.id where levels.id = ?", id, as: {String, String, Int32?, Int32?, Int32?, Bool, Bool, Bool, Int32, Int32, Int32?, String, Int32, String?, String?, String?, String?})
rescue err
LOG.error {"whar.... #{err}"}
context.response.status = HTTP::Status::NOT_FOUND
return
end
scores = DATABASE.query_all("select distinct percent, level_scores.coins, users.username, users.icon_type, users.color1, users.color2, users.cube, users.ship, users.ball, users.ufo, users.wave, users.robot, users.spider, users.special, set_at from level_scores join users on level_scores.account_id = users.account_id where level_id = ? order by percent desc, level_scores.coins desc, set_at limit 25", id, as: {Int32, Int32, String, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, String})
comments = DATABASE.query_all("select comment, comments.created_at, users.username, users.icon_type, users.color1, users.color2, users.cube, users.ship, users.ball, users.ufo, users.wave, users.robot, users.spider, users.special from comments left join users on comments.user_id = users.id where level_id = ? order by comments.created_at asc limit 20", id, as: {String, String, String, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32})
ECR.embed("./public/template/level.ecr", context.response)
}

View File

@ -3,7 +3,11 @@ require "http-session"
include CrystalGauntlet
CrystalGauntlet.template_endpoints["/login"] = ->(context : HTTP::Server::Context) {
CrystalGauntlet.template_endpoints[{
name: "login",
path: "/login",
methods: ["get", "post"]
}] = ->(context : HTTP::Server::Context, params : Hash(String, String?)) {
if session = CrystalGauntlet.sessions.get(context)
logged_in = true
account_id = session.account_id

View File

@ -3,12 +3,11 @@ require "http-session"
include CrystalGauntlet
CrystalGauntlet.template_endpoints["/accounts/logout"] = ->(context : HTTP::Server::Context) {
if context.request.method != "POST"
context.response.respond_with_status 405
return
end
CrystalGauntlet.template_endpoints[{
name: "logout",
path: "/accounts/logout",
methods: ["post"]
}] = ->(context : HTTP::Server::Context, params : Hash(String, String?)) {
CrystalGauntlet.sessions.delete(context)
context.response.headers.add("Location", "/")

View File

@ -1,6 +1,10 @@
include CrystalGauntlet
CrystalGauntlet.template_endpoints["/accounts/notifications"] = ->(context : HTTP::Server::Context) {
CrystalGauntlet.template_endpoints[{
name: "account_notifications",
path: "/accounts/notifications",
methods: ["get"]
}] = ->(context : HTTP::Server::Context, params : Hash(String, String?)) {
context.response.content_type = "text/html"
account_id = nil

View File

@ -3,7 +3,11 @@ require "xml"
include CrystalGauntlet
CrystalGauntlet.template_endpoints["/tools/reupload"] = ->(context : HTTP::Server::Context) {
CrystalGauntlet.template_endpoints[{
name: "reupload",
path: "/tools/reupload",
methods: ["get", "post"]
}] = ->(context : HTTP::Server::Context, params : Hash(String, String?)) {
context.response.content_type = "text/html"
disabled = !(config_get("reuploads.allowed").as?(Bool))

View File

@ -2,7 +2,11 @@ require "ecr"
include CrystalGauntlet
CrystalGauntlet.template_endpoints["/tools/song_search"] = ->(context : HTTP::Server::Context) {
CrystalGauntlet.template_endpoints[{
name: "song_search",
path: "/tools/song_search",
methods: ["get", "post"]
}] = ->(context : HTTP::Server::Context, params : Hash(String, String?)) {
context.response.content_type = "text/html"
error = nil

View File

@ -17,7 +17,11 @@ def get_next_song_id() : Int32
end
end
CrystalGauntlet.template_endpoints["/tools/song_upload"] = ->(context : HTTP::Server::Context) {
CrystalGauntlet.template_endpoints[{
name: "song_upload",
path: "/tools/song_upload",
methods: ["get", "post"]
}] = ->(context : HTTP::Server::Context, params : Hash(String, String?)) {
context.response.content_type = "text/html"
disabled = !(config_get("songs.allow_custom_songs").as?(Bool))