Initial commit
This commit is contained in:
0
handlers/__init__.py
Normal file
0
handlers/__init__.py
Normal file
78
handlers/apiCacheBeatmapHandler.py
Normal file
78
handlers/apiCacheBeatmapHandler.py
Normal file
@@ -0,0 +1,78 @@
|
||||
import json
|
||||
import sys
|
||||
import traceback
|
||||
|
||||
import tornado.gen
|
||||
import tornado.web
|
||||
from raven.contrib.tornado import SentryMixin
|
||||
|
||||
from objects import beatmap
|
||||
from common.log import logUtils as log
|
||||
from common.web import requestsManager
|
||||
from constants import exceptions
|
||||
from helpers import osuapiHelper
|
||||
from objects import glob
|
||||
from common.sentry import sentry
|
||||
|
||||
MODULE_NAME = "api/cacheBeatmap"
|
||||
class handler(requestsManager.asyncRequestHandler):
|
||||
"""
|
||||
Handler for /api/v1/cacheBeatmap
|
||||
"""
|
||||
@tornado.web.asynchronous
|
||||
@tornado.gen.engine
|
||||
@sentry.captureTornado
|
||||
def asyncPost(self):
|
||||
statusCode = 400
|
||||
data = {"message": "unknown error"}
|
||||
try:
|
||||
# Check arguments
|
||||
if not requestsManager.checkArguments(self.request.arguments, ["sid", "refresh"]):
|
||||
raise exceptions.invalidArgumentsException(MODULE_NAME)
|
||||
|
||||
# Get beatmap set data from osu api
|
||||
beatmapSetID = self.get_argument("sid")
|
||||
refresh = int(self.get_argument("refresh"))
|
||||
if refresh == 1:
|
||||
log.debug("Forced refresh")
|
||||
apiResponse = osuapiHelper.osuApiRequest("get_beatmaps", "s={}".format(beatmapSetID), False)
|
||||
if len(apiResponse) == 0:
|
||||
raise exceptions.invalidBeatmapException
|
||||
|
||||
# Loop through all beatmaps in this set and save them in db
|
||||
data["maps"] = []
|
||||
for i in apiResponse:
|
||||
log.debug("Saving beatmap {} in db".format(i["file_md5"]))
|
||||
bmap = beatmap.beatmap(i["file_md5"], int(i["beatmapset_id"]), refresh=refresh)
|
||||
pp = glob.db.fetch("SELECT pp_100 FROM beatmaps WHERE beatmap_id = %s LIMIT 1", [bmap.beatmapID])
|
||||
if pp is None:
|
||||
pp = 0
|
||||
else:
|
||||
pp = pp["pp_100"]
|
||||
data["maps"].append({
|
||||
"id": bmap.beatmapID,
|
||||
"name": bmap.songName,
|
||||
"status": bmap.rankedStatus,
|
||||
"frozen": bmap.rankedStatusFrozen,
|
||||
"pp": pp,
|
||||
})
|
||||
|
||||
# Set status code and message
|
||||
statusCode = 200
|
||||
data["message"] = "ok"
|
||||
except exceptions.invalidArgumentsException:
|
||||
# Set error and message
|
||||
statusCode = 400
|
||||
data["message"] = "missing required arguments"
|
||||
except exceptions.invalidBeatmapException:
|
||||
statusCode = 400
|
||||
data["message"] = "beatmap not found from osu!api."
|
||||
finally:
|
||||
# Add status code to data
|
||||
data["status"] = statusCode
|
||||
|
||||
# Send response
|
||||
self.write(json.dumps(data))
|
||||
self.set_header("Content-Type", "application/json")
|
||||
#self.add_header("Access-Control-Allow-Origin", "*")
|
||||
self.set_status(statusCode)
|
176
handlers/apiPPHandler.py
Normal file
176
handlers/apiPPHandler.py
Normal file
@@ -0,0 +1,176 @@
|
||||
import json
|
||||
import sys
|
||||
import traceback
|
||||
|
||||
import tornado.gen
|
||||
import tornado.web
|
||||
from raven.contrib.tornado import SentryMixin
|
||||
|
||||
from objects import beatmap
|
||||
from common.constants import gameModes
|
||||
from common.log import logUtils as log
|
||||
from common.web import requestsManager
|
||||
from constants import exceptions
|
||||
from helpers import osuapiHelper
|
||||
from objects import glob
|
||||
from pp import rippoppai
|
||||
from pp import rxoppai
|
||||
from common.sentry import sentry
|
||||
|
||||
MODULE_NAME = "api/pp"
|
||||
class handler(requestsManager.asyncRequestHandler):
|
||||
"""
|
||||
Handler for /api/v1/pp
|
||||
"""
|
||||
@tornado.web.asynchronous
|
||||
@tornado.gen.engine
|
||||
@sentry.captureTornado
|
||||
def asyncGet(self):
|
||||
statusCode = 400
|
||||
data = {"message": "unknown error"}
|
||||
try:
|
||||
# Check arguments
|
||||
if not requestsManager.checkArguments(self.request.arguments, ["b"]):
|
||||
raise exceptions.invalidArgumentsException(MODULE_NAME)
|
||||
|
||||
# Get beatmap ID and make sure it's a valid number
|
||||
beatmapID = self.get_argument("b")
|
||||
if not beatmapID.isdigit():
|
||||
raise exceptions.invalidArgumentsException(MODULE_NAME)
|
||||
|
||||
# Get mods
|
||||
if "m" in self.request.arguments:
|
||||
modsEnum = self.get_argument("m")
|
||||
if not modsEnum.isdigit():
|
||||
raise exceptions.invalidArgumentsException(MODULE_NAME)
|
||||
modsEnum = int(modsEnum)
|
||||
else:
|
||||
modsEnum = 0
|
||||
|
||||
# Get game mode
|
||||
if "g" in self.request.arguments:
|
||||
gameMode = self.get_argument("g")
|
||||
if not gameMode.isdigit():
|
||||
raise exceptions.invalidArgumentsException(MODULE_NAME)
|
||||
gameMode = int(gameMode)
|
||||
else:
|
||||
gameMode = 0
|
||||
|
||||
# Get acc
|
||||
if "a" in self.request.arguments:
|
||||
accuracy = self.get_argument("a")
|
||||
try:
|
||||
accuracy = float(accuracy)
|
||||
except ValueError:
|
||||
raise exceptions.invalidArgumentsException(MODULE_NAME)
|
||||
else:
|
||||
accuracy = -1.0
|
||||
|
||||
# Print message
|
||||
log.info("Requested pp for beatmap {}".format(beatmapID))
|
||||
|
||||
# Get beatmap md5 from osuapi
|
||||
# TODO: Move this to beatmap object
|
||||
osuapiData = osuapiHelper.osuApiRequest("get_beatmaps", "b={}".format(beatmapID))
|
||||
if osuapiData is None or "file_md5" not in osuapiData or "beatmapset_id" not in osuapiData:
|
||||
raise exceptions.invalidBeatmapException(MODULE_NAME)
|
||||
beatmapMd5 = osuapiData["file_md5"]
|
||||
beatmapSetID = osuapiData["beatmapset_id"]
|
||||
|
||||
# Create beatmap object
|
||||
bmap = beatmap.beatmap(beatmapMd5, beatmapSetID)
|
||||
|
||||
# Check beatmap length
|
||||
if bmap.hitLength > 900:
|
||||
raise exceptions.beatmapTooLongException(MODULE_NAME)
|
||||
|
||||
returnPP = []
|
||||
if gameMode == gameModes.STD and bmap.starsStd == 0:
|
||||
# Mode Specific beatmap, auto detect game mode
|
||||
if bmap.starsTaiko > 0:
|
||||
gameMode = gameModes.TAIKO
|
||||
if bmap.starsCtb > 0:
|
||||
gameMode = gameModes.CTB
|
||||
if bmap.starsMania > 0:
|
||||
gameMode = gameModes.MANIA
|
||||
|
||||
# Calculate pp
|
||||
if gameMode == gameModes.STD or gameMode == gameModes.TAIKO:
|
||||
# Std pp
|
||||
if accuracy < 0 and modsEnum == 0:
|
||||
# Generic acc
|
||||
# Get cached pp values
|
||||
cachedPP = bmap.getCachedTillerinoPP()
|
||||
if cachedPP != [0,0,0,0]:
|
||||
log.debug("Got cached pp.")
|
||||
returnPP = cachedPP
|
||||
else:
|
||||
log.debug("Cached pp not found. Calculating pp with oppai...")
|
||||
# Cached pp not found, calculate them
|
||||
oppai = rippoppai.oppai(bmap, mods=modsEnum, tillerino=True)
|
||||
returnPP = oppai.pp
|
||||
bmap.starsStd = oppai.stars
|
||||
|
||||
# Cache values in DB
|
||||
log.debug("Saving cached pp...")
|
||||
if type(returnPP) == list and len(returnPP) == 4:
|
||||
bmap.saveCachedTillerinoPP(returnPP)
|
||||
else:
|
||||
# Specific accuracy, calculate
|
||||
# Create oppai instance
|
||||
log.debug("Specific request ({}%/{}). Calculating pp with oppai...".format(accuracy, modsEnum))
|
||||
if modsEnum & 128:
|
||||
oppai = rxoppai.oppai(bmap, mods=modsEnum, tillerino=True)
|
||||
else:
|
||||
oppai = rippoppai.oppai(bmap, mods=modsEnum, tillerino=True)
|
||||
bmap.starsStd = oppai.stars
|
||||
if accuracy > 0:
|
||||
returnPP.append(calculatePPFromAcc(oppai, accuracy))
|
||||
else:
|
||||
returnPP = oppai.pp
|
||||
else:
|
||||
raise exceptions.unsupportedGameModeException()
|
||||
|
||||
# Data to return
|
||||
data = {
|
||||
"song_name": bmap.songName,
|
||||
"pp": [round(x, 2) for x in returnPP] if type(returnPP) == list else returnPP,
|
||||
"length": bmap.hitLength,
|
||||
"stars": bmap.starsStd,
|
||||
"ar": bmap.AR,
|
||||
"bpm": bmap.bpm,
|
||||
}
|
||||
|
||||
# Set status code and message
|
||||
statusCode = 200
|
||||
data["message"] = "ok"
|
||||
except exceptions.invalidArgumentsException:
|
||||
# Set error and message
|
||||
statusCode = 400
|
||||
data["message"] = "missing required arguments"
|
||||
except exceptions.invalidBeatmapException:
|
||||
statusCode = 400
|
||||
data["message"] = "beatmap not found"
|
||||
except exceptions.beatmapTooLongException:
|
||||
statusCode = 400
|
||||
data["message"] = "requested beatmap is too long"
|
||||
except exceptions.unsupportedGameModeException:
|
||||
statusCode = 400
|
||||
data["message"] = "Unsupported gamemode"
|
||||
finally:
|
||||
# Add status code to data
|
||||
data["status"] = statusCode
|
||||
|
||||
# Debug output
|
||||
log.debug(str(data))
|
||||
|
||||
# Send response
|
||||
#self.clear()
|
||||
self.write(json.dumps(data))
|
||||
self.set_header("Content-Type", "application/json")
|
||||
self.set_status(statusCode)
|
||||
|
||||
def calculatePPFromAcc(ppcalc, acc):
|
||||
ppcalc.acc = acc
|
||||
ppcalc.calculatePP()
|
||||
return ppcalc.pp
|
12
handlers/apiStatusHandler.py
Normal file
12
handlers/apiStatusHandler.py
Normal file
@@ -0,0 +1,12 @@
|
||||
import json
|
||||
|
||||
from common.web import requestsManager
|
||||
|
||||
|
||||
class handler(requestsManager.asyncRequestHandler):
|
||||
"""
|
||||
Handler for /api/v1/status
|
||||
"""
|
||||
def asyncGet(self):
|
||||
self.write(json.dumps({"status": 200, "server_status": 1}))
|
||||
#self.finish()
|
70
handlers/banchoConnectHandler.py
Normal file
70
handlers/banchoConnectHandler.py
Normal file
@@ -0,0 +1,70 @@
|
||||
import sys
|
||||
import traceback
|
||||
|
||||
import tornado.gen
|
||||
import tornado.web
|
||||
from raven.contrib.tornado import SentryMixin
|
||||
|
||||
from common.log import logUtils as log
|
||||
from common.ripple import userUtils
|
||||
from common.web import requestsManager
|
||||
from constants import exceptions
|
||||
from objects import glob
|
||||
from common.sentry import sentry
|
||||
|
||||
MODULE_NAME = "bancho_connect"
|
||||
class handler(requestsManager.asyncRequestHandler):
|
||||
"""
|
||||
Handler for /web/bancho_connect.php
|
||||
"""
|
||||
@tornado.web.asynchronous
|
||||
@tornado.gen.engine
|
||||
@sentry.captureTornado
|
||||
def asyncGet(self):
|
||||
try:
|
||||
# Get request ip
|
||||
ip = self.getRequestIP()
|
||||
|
||||
# Argument check
|
||||
if not requestsManager.checkArguments(self.request.arguments, ["u", "h"]):
|
||||
raise exceptions.invalidArgumentsException(MODULE_NAME)
|
||||
|
||||
# Get user ID
|
||||
username = self.get_argument("u")
|
||||
userID = userUtils.getID(username)
|
||||
if userID is None:
|
||||
raise exceptions.loginFailedException(MODULE_NAME, username)
|
||||
|
||||
# Check login
|
||||
log.info("{} ({}) wants to connect".format(username, userID))
|
||||
if not userUtils.checkLogin(userID, self.get_argument("h"), ip):
|
||||
raise exceptions.loginFailedException(MODULE_NAME, username)
|
||||
|
||||
# Ban check
|
||||
if userUtils.isBanned(userID):
|
||||
raise exceptions.userBannedException(MODULE_NAME, username)
|
||||
|
||||
# Lock check
|
||||
if userUtils.isLocked(userID):
|
||||
raise exceptions.userLockedException(MODULE_NAME, username)
|
||||
|
||||
# 2FA check
|
||||
if userUtils.check2FA(userID, ip):
|
||||
raise exceptions.need2FAException(MODULE_NAME, username, ip)
|
||||
|
||||
# Update latest activity
|
||||
userUtils.updateLatestActivity(userID)
|
||||
|
||||
# Get country and output it
|
||||
country = glob.db.fetch("SELECT country FROM users_stats WHERE id = %s", [userID])["country"]
|
||||
self.write(country)
|
||||
except exceptions.invalidArgumentsException:
|
||||
pass
|
||||
except exceptions.loginFailedException:
|
||||
self.write("error: pass\n")
|
||||
except exceptions.userBannedException:
|
||||
pass
|
||||
except exceptions.userLockedException:
|
||||
pass
|
||||
except exceptions.need2FAException:
|
||||
self.write("error: verify\n")
|
36
handlers/checkUpdatesHandler.py
Normal file
36
handlers/checkUpdatesHandler.py
Normal file
@@ -0,0 +1,36 @@
|
||||
from urllib.parse import urlencode
|
||||
|
||||
import requests
|
||||
import tornado.gen
|
||||
import tornado.web
|
||||
|
||||
from common.log import logUtils as log
|
||||
from common.web import requestsManager
|
||||
|
||||
|
||||
class handler(requestsManager.asyncRequestHandler):
|
||||
@tornado.web.asynchronous
|
||||
@tornado.gen.engine
|
||||
def asyncGet(self):
|
||||
try:
|
||||
args = {}
|
||||
#if "stream" in self.request.arguments:
|
||||
# args["stream"] = self.get_argument("stream")
|
||||
#if "action" in self.request.arguments:
|
||||
# args["action"] = self.get_argument("action")
|
||||
#if "time" in self.request.arguments:
|
||||
# args["time"] = self.get_argument("time")
|
||||
|
||||
# Pass all arguments otherwise it doesn't work
|
||||
for key, _ in self.request.arguments.items():
|
||||
args[key] = self.get_argument(key)
|
||||
|
||||
if args["action"].lower() == "put":
|
||||
self.write("nope")
|
||||
return
|
||||
|
||||
response = requests.get("https://osu.ppy.sh/web/check-updates.php?{}".format(urlencode(args)))
|
||||
self.write(response.text)
|
||||
except Exception as e:
|
||||
log.error("check-updates failed: {}".format(e))
|
||||
self.write("")
|
175
handlers/commentHandler.py
Normal file
175
handlers/commentHandler.py
Normal file
@@ -0,0 +1,175 @@
|
||||
import tornado.gen
|
||||
import tornado.web
|
||||
|
||||
from common.log import logUtils as log
|
||||
from common.ripple import userUtils
|
||||
from common.sentry import sentry
|
||||
from common.web import requestsManager
|
||||
from constants import exceptions
|
||||
from objects import glob
|
||||
|
||||
MODULE_NAME = "comments"
|
||||
|
||||
class handler(requestsManager.asyncRequestHandler):
|
||||
CLIENT_WHO = {"normal": "", "player": "player", "admin": "bat", "donor": "subscriber"}
|
||||
|
||||
@tornado.web.asynchronous
|
||||
@tornado.gen.engine
|
||||
@sentry.captureTornado
|
||||
def asyncPost(self):
|
||||
try:
|
||||
# Required arguments check
|
||||
if not requestsManager.checkArguments(self.request.arguments, ("u", "p", "a")):
|
||||
raise exceptions.invalidArgumentsException(MODULE_NAME)
|
||||
|
||||
# Get arguments
|
||||
username = self.get_argument("u")
|
||||
password = self.get_argument("p")
|
||||
action = self.get_argument("a").strip().lower()
|
||||
|
||||
# IP for session check
|
||||
ip = self.getRequestIP()
|
||||
|
||||
# Login and ban check
|
||||
userID = userUtils.getID(username)
|
||||
if userID == 0:
|
||||
raise exceptions.loginFailedException(MODULE_NAME, userID)
|
||||
if not userUtils.checkLogin(userID, password, ip):
|
||||
raise exceptions.loginFailedException(MODULE_NAME, username)
|
||||
if userUtils.check2FA(userID, ip):
|
||||
raise exceptions.need2FAException(MODULE_NAME, userID, ip)
|
||||
if userUtils.isBanned(userID):
|
||||
raise exceptions.userBannedException(MODULE_NAME, username)
|
||||
|
||||
# Action (depends on 'action' parameter, not on HTTP method)
|
||||
if action == "get":
|
||||
self.write(self._getComments())
|
||||
elif action == "post":
|
||||
self._addComment()
|
||||
except (exceptions.loginFailedException, exceptions.need2FAException, exceptions.userBannedException):
|
||||
self.write("error: no")
|
||||
|
||||
@staticmethod
|
||||
def clientWho(y):
|
||||
return handler.CLIENT_WHO[y["who"]] + (
|
||||
("|{}".format(y["special_format"])) if y["special_format"] is not None else ""
|
||||
)
|
||||
|
||||
def _getComments(self):
|
||||
output = ""
|
||||
|
||||
try:
|
||||
beatmapID = int(self.get_argument("b", default=0))
|
||||
beatmapSetID = int(self.get_argument("s", default=0))
|
||||
scoreID = int(self.get_argument("r", default=0))
|
||||
except ValueError:
|
||||
raise exceptions.invalidArgumentsException(MODULE_NAME)
|
||||
|
||||
if beatmapID <= 0:
|
||||
return
|
||||
|
||||
log.info("Requested comments for beatmap id {}".format(beatmapID))
|
||||
|
||||
# Merge beatmap, beatmapset and score comments
|
||||
for x in (
|
||||
{"db_type": "beatmap_id", "client_type": "map", "value": beatmapID},
|
||||
{"db_type": "beatmapset_id", "client_type": "song", "value": beatmapSetID},
|
||||
{"db_type": "score_id", "client_type": "replay", "value": scoreID},
|
||||
):
|
||||
# Add this set of comments only if the client has set the value
|
||||
if x["value"] <= 0:
|
||||
continue
|
||||
|
||||
# Fetch these comments
|
||||
comments = glob.db.fetchAll(
|
||||
"SELECT * FROM comments WHERE {} = %s ORDER BY `time`".format(x["db_type"]),
|
||||
(x["value"],)
|
||||
)
|
||||
|
||||
# Output comments
|
||||
output += "\n".join([
|
||||
"{y[time]}\t{client_name}\t{client_who}\t{y[comment]}".format(
|
||||
y=y,
|
||||
client_name=x["client_type"],
|
||||
client_who=self.clientWho(y)
|
||||
) for y in comments
|
||||
]) + "\n"
|
||||
return output
|
||||
|
||||
def _addComment(self):
|
||||
username = self.get_argument("u")
|
||||
target = self.get_argument("target", default=None)
|
||||
specialFormat = self.get_argument("f", default=None)
|
||||
userID = userUtils.getID(username)
|
||||
|
||||
# Technically useless
|
||||
if userID < 0:
|
||||
return
|
||||
|
||||
# Get beatmap/set/score ids
|
||||
try:
|
||||
beatmapID = int(self.get_argument("b", default=0))
|
||||
beatmapSetID = int(self.get_argument("s", default=0))
|
||||
scoreID = int(self.get_argument("r", default=0))
|
||||
except ValueError:
|
||||
raise exceptions.invalidArgumentsException(MODULE_NAME)
|
||||
|
||||
# Add a comment, removing all illegal characters and trimming after 128 characters
|
||||
comment = self.get_argument("comment").replace("\r", "").replace("\t", "").replace("\n", "")[:128]
|
||||
try:
|
||||
time_ = int(self.get_argument("starttime"))
|
||||
except ValueError:
|
||||
raise exceptions.invalidArgumentsException(MODULE_NAME)
|
||||
|
||||
# Type of comment
|
||||
who = "normal"
|
||||
if target == "replay" and glob.db.fetch(
|
||||
"SELECT COUNT(*) AS c FROM scores WHERE id = %s AND userid = %s AND completed = 3",
|
||||
(scoreID, userID)
|
||||
)["c"] > 0:
|
||||
# From player, on their score
|
||||
who = "player"
|
||||
elif userUtils.isInAnyPrivilegeGroup(userID, ("super admin", "developer", "community manager", "bat")):
|
||||
# From BAT/Admin
|
||||
who = "admin"
|
||||
elif userUtils.isInPrivilegeGroup(userID, "premium"):
|
||||
# Akatsuki Premium Member
|
||||
who = "donor"
|
||||
|
||||
if target == "song":
|
||||
# Set comment
|
||||
if beatmapSetID <= 0:
|
||||
return
|
||||
value = beatmapSetID
|
||||
column = "beatmapset_id"
|
||||
elif target == "map":
|
||||
# Beatmap comment
|
||||
if beatmapID <= 0:
|
||||
return
|
||||
value = beatmapID
|
||||
column = "beatmap_id"
|
||||
elif target == "replay":
|
||||
# Score comment
|
||||
if scoreID <= 0:
|
||||
return
|
||||
value = scoreID
|
||||
column = "score_id"
|
||||
else:
|
||||
# Invalid target
|
||||
return
|
||||
|
||||
# Make sure the user hasn't submitted another comment on the same map/set/song in a 5 seconds range
|
||||
if glob.db.fetch(
|
||||
"SELECT COUNT(*) AS c FROM comments WHERE user_id = %s AND {} = %s AND `time` BETWEEN %s AND %s".format(
|
||||
column
|
||||
), (userID, value, time_ - 5000, time_ + 5000)
|
||||
)["c"] > 0:
|
||||
return
|
||||
|
||||
# Store the comment
|
||||
glob.db.execute(
|
||||
"INSERT INTO comments ({}, user_id, comment, `time`, who, special_format) "
|
||||
"VALUES (%s, %s, %s, %s, %s, %s)".format(column),
|
||||
(value, userID, comment, time_, who, specialFormat)
|
||||
)
|
||||
log.info("Submitted {} ({}) comment, user {}: '{}'".format(column, value, userID, comment))
|
53
handlers/defaultHandler.py
Normal file
53
handlers/defaultHandler.py
Normal file
@@ -0,0 +1,53 @@
|
||||
import tornado.gen
|
||||
import tornado.web
|
||||
|
||||
from common.web import requestsManager
|
||||
|
||||
|
||||
class handler(requestsManager.asyncRequestHandler):
|
||||
@tornado.web.asynchronous
|
||||
@tornado.gen.engine
|
||||
def asyncGet(self):
|
||||
print("404: {}".format(self.request.uri))
|
||||
self.write("""
|
||||
<html>
|
||||
<head>
|
||||
<style>
|
||||
@import url(https://fonts.googleapis.com/css?family=Source+Sans+Pro:400,700,600,400italic,600italic,700italic,900,900italic);
|
||||
@import url(https://fonts.googleapis.com/css?family=Raleway:400,700);
|
||||
html, body {
|
||||
height: 90%;
|
||||
background-image: url(http://y.zxq.co/xtffuu.png);
|
||||
}
|
||||
.main {
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
display: table;
|
||||
}
|
||||
.wrapper {
|
||||
display: table-cell;
|
||||
height: 90%;
|
||||
vertical-align: middle;
|
||||
}
|
||||
body {
|
||||
font-family: Source Sans Pro;
|
||||
text-align: center;
|
||||
}
|
||||
h1, h2, h3, h4, h5, h6 {
|
||||
font-family: Raleway;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class = "main">
|
||||
<div class = "wrapper">
|
||||
<a href="https://akatsuki.pw"><img src="https://i.namir.in//Mbp.png"></a>
|
||||
<h3>Howdy, you're still connected to Akatsuki!</h3>
|
||||
You can't access osu!'s website if the Server Switcher is On.<br>
|
||||
Please open the <b>Server Switcher</b> and click <b>On/Off</b> to switch server, then refresh this page.
|
||||
<h4>If you still can't access osu! website even if the switcher is Off, <a href="http://www.refreshyourcache.com/" target="_blank">clean your browser cache</a>.</h4>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
""")
|
29
handlers/downloadMapHandler.py
Normal file
29
handlers/downloadMapHandler.py
Normal file
@@ -0,0 +1,29 @@
|
||||
import tornado.gen
|
||||
import tornado.web
|
||||
|
||||
from common.web import requestsManager
|
||||
from common.sentry import sentry
|
||||
|
||||
MODULE_NAME = "direct_download"
|
||||
class handler(requestsManager.asyncRequestHandler):
|
||||
"""
|
||||
Handler for /d/
|
||||
"""
|
||||
@tornado.web.asynchronous
|
||||
@tornado.gen.engine
|
||||
@sentry.captureTornado
|
||||
def asyncGet(self, bid):
|
||||
try:
|
||||
noVideo = bid.endswith("n")
|
||||
if noVideo:
|
||||
bid = bid[:-1]
|
||||
bid = int(bid)
|
||||
|
||||
self.set_status(302, "Moved Temporarily")
|
||||
url = "https://bm6.ppy.sh/d/{}{}".format(bid, "?novideo" if noVideo else "")
|
||||
self.add_header("Location", url)
|
||||
self.add_header("Cache-Control", "no-cache")
|
||||
self.add_header("Pragma", "no-cache")
|
||||
except ValueError:
|
||||
self.set_status(400)
|
||||
self.write("Invalid set id")
|
12
handlers/emptyHandler.py
Normal file
12
handlers/emptyHandler.py
Normal file
@@ -0,0 +1,12 @@
|
||||
import tornado.gen
|
||||
import tornado.web
|
||||
|
||||
from common.web import requestsManager
|
||||
|
||||
|
||||
class handler(requestsManager.asyncRequestHandler):
|
||||
@tornado.web.asynchronous
|
||||
@tornado.gen.engine
|
||||
def asyncGet(self):
|
||||
#self.set_status(404)
|
||||
self.write("Not yet")
|
31
handlers/getFullReplayHandler.py
Normal file
31
handlers/getFullReplayHandler.py
Normal file
@@ -0,0 +1,31 @@
|
||||
import tornado.gen
|
||||
import tornado.web
|
||||
|
||||
from common.web import requestsManager
|
||||
from constants import exceptions
|
||||
from helpers import replayHelper
|
||||
from common.sentry import sentry
|
||||
|
||||
MODULE_NAME = "get_full_replay"
|
||||
class handler(requestsManager.asyncRequestHandler):
|
||||
"""
|
||||
Handler for /replay/
|
||||
"""
|
||||
@tornado.web.asynchronous
|
||||
@tornado.gen.engine
|
||||
@sentry.captureTornado
|
||||
def asyncGet(self, replayID):
|
||||
try:
|
||||
fullReplay = replayHelper.buildFullReplay(scoreID=replayID)
|
||||
self.write(fullReplay)
|
||||
self.add_header("Content-type", "application/octet-stream")
|
||||
self.set_header("Content-length", len(fullReplay))
|
||||
self.set_header("Content-Description", "File Transfer")
|
||||
self.set_header("Content-Disposition", "attachment; filename=\"{}.osr\"".format(replayID))
|
||||
except (exceptions.fileNotFoundException, exceptions.scoreNotFoundError):
|
||||
fullReplay = replayHelper.rxbuildFullReplay(scoreID=replayID)
|
||||
self.write(fullReplay)
|
||||
self.add_header("Content-type", "application/octet-stream")
|
||||
self.set_header("Content-length", len(fullReplay))
|
||||
self.set_header("Content-Description", "File Transfer")
|
||||
self.set_header("Content-Disposition", "attachment; filename=\"{}.osr\"".format(replayID))
|
79
handlers/getReplayHandler.py
Normal file
79
handlers/getReplayHandler.py
Normal file
@@ -0,0 +1,79 @@
|
||||
import os
|
||||
import sys
|
||||
import traceback
|
||||
|
||||
import tornado.gen
|
||||
import tornado.web
|
||||
from raven.contrib.tornado import SentryMixin
|
||||
|
||||
from common.log import logUtils as log
|
||||
from common.ripple import userUtils
|
||||
from common.web import requestsManager
|
||||
from constants import exceptions
|
||||
from common.constants import mods
|
||||
from objects import glob
|
||||
from objects import rxscore
|
||||
from common.sentry import sentry
|
||||
|
||||
MODULE_NAME = "get_replay"
|
||||
class handler(requestsManager.asyncRequestHandler):
|
||||
"""
|
||||
Handler for osu-getreplay.php
|
||||
"""
|
||||
@tornado.web.asynchronous
|
||||
@tornado.gen.engine
|
||||
@sentry.captureTornado
|
||||
def asyncGet(self):
|
||||
try:
|
||||
# Get request ip
|
||||
ip = self.getRequestIP()
|
||||
|
||||
# Check arguments
|
||||
if not requestsManager.checkArguments(self.request.arguments, ["c", "u", "h"]):
|
||||
raise exceptions.invalidArgumentsException(MODULE_NAME)
|
||||
|
||||
# Get arguments
|
||||
username = self.get_argument("u")
|
||||
password = self.get_argument("h")
|
||||
replayID = self.get_argument("c")
|
||||
s = rxscore.score()
|
||||
# Login check
|
||||
userID = userUtils.getID(username)
|
||||
if userID == 0:
|
||||
raise exceptions.loginFailedException(MODULE_NAME, userID)
|
||||
if not userUtils.checkLogin(userID, password, ip):
|
||||
raise exceptions.loginFailedException(MODULE_NAME, username)
|
||||
if userUtils.check2FA(userID, ip):
|
||||
raise exceptions.need2FAException(MODULE_NAME, username, ip)
|
||||
|
||||
# Get user ID
|
||||
if bool(s.mods & 128): # Relax
|
||||
replayData = glob.db.fetch("SELECT scores_relax.*, users.username AS uname FROM scores_relax LEFT JOIN users ON scores_relax.userid = users.id WHERE scores_relax.id = %s", [replayID])
|
||||
# Increment 'replays watched by others' if needed
|
||||
if replayData is not None:
|
||||
if username != replayData["uname"]:
|
||||
userUtils.incrementReplaysWatched(replayData["userid"], replayData["play_mode"], s.mods)
|
||||
else:
|
||||
replayData = glob.db.fetch("SELECT scores.*, users.username AS uname FROM scores LEFT JOIN users ON scores.userid = users.id WHERE scores.id = %s", [replayID])
|
||||
# Increment 'replays watched by others' if needed
|
||||
if replayData is not None:
|
||||
if username != replayData["uname"]:
|
||||
userUtils.incrementReplaysWatched(replayData["userid"], replayData["play_mode"], s.mods)
|
||||
|
||||
|
||||
log.info("Serving replay_{}.osr".format(replayID))
|
||||
fileName = ".data/replays/replay_{}.osr".format(replayID)
|
||||
if os.path.isfile(fileName):
|
||||
with open(fileName, "rb") as f:
|
||||
fileContent = f.read()
|
||||
self.write(fileContent)
|
||||
else:
|
||||
self.write("")
|
||||
log.warning("Replay {} doesn't exist.".format(replayID))
|
||||
|
||||
except exceptions.invalidArgumentsException:
|
||||
pass
|
||||
except exceptions.need2FAException:
|
||||
pass
|
||||
except exceptions.loginFailedException:
|
||||
pass
|
121
handlers/getScoresHandler.pyx
Normal file
121
handlers/getScoresHandler.pyx
Normal file
@@ -0,0 +1,121 @@
|
||||
import json
|
||||
import tornado.gen
|
||||
import tornado.web
|
||||
|
||||
from objects import beatmap
|
||||
from objects import scoreboard
|
||||
from objects import relaxboard
|
||||
from common.constants import privileges
|
||||
from common.log import logUtils as log
|
||||
from common.ripple import userUtils
|
||||
from common.web import requestsManager
|
||||
from constants import exceptions
|
||||
from objects import glob
|
||||
from common.constants import mods
|
||||
from common.sentry import sentry
|
||||
|
||||
MODULE_NAME = "get_scores"
|
||||
class handler(requestsManager.asyncRequestHandler):
|
||||
"""
|
||||
Handler for /web/osu-osz2-getscores.php
|
||||
"""
|
||||
@tornado.web.asynchronous
|
||||
@tornado.gen.engine
|
||||
@sentry.captureTornado
|
||||
def asyncGet(self):
|
||||
try:
|
||||
# Get request ip
|
||||
ip = self.getRequestIP()
|
||||
|
||||
# Print arguments
|
||||
if glob.debug:
|
||||
requestsManager.printArguments(self)
|
||||
|
||||
# TODO: Maintenance check
|
||||
|
||||
# Check required arguments
|
||||
if not requestsManager.checkArguments(self.request.arguments, ["c", "f", "i", "m", "us", "v", "vv", "mods"]):
|
||||
raise exceptions.invalidArgumentsException(MODULE_NAME)
|
||||
|
||||
# GET parameters
|
||||
md5 = self.get_argument("c")
|
||||
fileName = self.get_argument("f")
|
||||
beatmapSetID = self.get_argument("i")
|
||||
gameMode = self.get_argument("m")
|
||||
username = self.get_argument("us")
|
||||
password = self.get_argument("ha")
|
||||
scoreboardType = int(self.get_argument("v"))
|
||||
scoreboardVersion = int(self.get_argument("vv"))
|
||||
|
||||
# Login and ban check
|
||||
userID = userUtils.getID(username)
|
||||
if userID == 0:
|
||||
raise exceptions.loginFailedException(MODULE_NAME, userID)
|
||||
if not userUtils.checkLogin(userID, password, ip):
|
||||
raise exceptions.loginFailedException(MODULE_NAME, username)
|
||||
if userUtils.check2FA(userID, ip):
|
||||
raise exceptions.need2FAException(MODULE_NAME, username, ip)
|
||||
# Ban check is pointless here, since there's no message on the client
|
||||
#if userHelper.isBanned(userID) == True:
|
||||
# raise exceptions.userBannedException(MODULE_NAME, username)
|
||||
|
||||
# Hax check
|
||||
if "a" in self.request.arguments:
|
||||
if int(self.get_argument("a")) == 1 and not userUtils.getAqn(userID):
|
||||
log.warning("Found AQN folder on user {} ({})".format(username, userID), "cm")
|
||||
userUtils.setAqn(userID)
|
||||
|
||||
# Scoreboard type
|
||||
isDonor = userUtils.getPrivileges(userID) & privileges.USER_DONOR > 0
|
||||
country = False
|
||||
friends = False
|
||||
modsFilter = -1
|
||||
mods = int(self.get_argument("mods"))
|
||||
if scoreboardType == 4:
|
||||
# Country leaderboard
|
||||
country = True
|
||||
elif scoreboardType == 2:
|
||||
# Mods leaderboard, replace mods (-1, every mod) with "mods" GET parameters
|
||||
modsFilter = int(self.get_argument("mods"))
|
||||
|
||||
elif scoreboardType == 3 and isDonor:
|
||||
# Friends leaderboard
|
||||
friends = True
|
||||
|
||||
# Console output
|
||||
fileNameShort = fileName[:32]+"..." if len(fileName) > 32 else fileName[:-4]
|
||||
if scoreboardType == 1 and int(self.get_argument("mods")) & 128:
|
||||
log.info("[RELAX] Requested beatmap {} ({})".format(fileNameShort, md5))
|
||||
else:
|
||||
log.info("[VANILLA] Requested beatmap {} ({})".format(fileNameShort, md5))
|
||||
|
||||
# Create beatmap object and set its data
|
||||
bmap = beatmap.beatmap(md5, beatmapSetID, gameMode)
|
||||
|
||||
if int(self.get_argument("mods")) & 128:
|
||||
glob.redis.publish("peppy:update_rxcached_stats", userID)
|
||||
else:
|
||||
glob.redis.publish("peppy:update_cached_stats", userID)
|
||||
|
||||
if bool(mods & 128):
|
||||
sboard = relaxboard.scoreboard(username, gameMode, bmap, setScores=True, country=country, mods=modsFilter, friends=friends)
|
||||
else:
|
||||
sboard = scoreboard.scoreboard(username, gameMode, bmap, setScores=True, country=country, mods=modsFilter, friends=friends)
|
||||
|
||||
# Data to return
|
||||
data = ""
|
||||
data += bmap.getData(sboard.totalScores, scoreboardVersion)
|
||||
data += sboard.getScoresData()
|
||||
self.write(data)
|
||||
|
||||
|
||||
# Datadog stats
|
||||
glob.dog.increment(glob.DATADOG_PREFIX+".served_leaderboards")
|
||||
except exceptions.need2FAException:
|
||||
self.write("error: 2fa")
|
||||
except exceptions.invalidArgumentsException:
|
||||
self.write("error: meme")
|
||||
except exceptions.userBannedException:
|
||||
self.write("error: ban")
|
||||
except exceptions.loginFailedException:
|
||||
self.write("error: pass")
|
41
handlers/getScreenshotHandler.py
Normal file
41
handlers/getScreenshotHandler.py
Normal file
@@ -0,0 +1,41 @@
|
||||
import os
|
||||
import sys
|
||||
import traceback
|
||||
|
||||
import tornado.gen
|
||||
import tornado.web
|
||||
from raven.contrib.tornado import SentryMixin
|
||||
|
||||
from common.log import logUtils as log
|
||||
from common.web import requestsManager
|
||||
from constants import exceptions
|
||||
from objects import glob
|
||||
from common.sentry import sentry
|
||||
|
||||
MODULE_NAME = "get_screenshot"
|
||||
class handler(requestsManager.asyncRequestHandler):
|
||||
"""
|
||||
Handler for /ss/
|
||||
"""
|
||||
@tornado.web.asynchronous
|
||||
@tornado.gen.engine
|
||||
@sentry.captureTornado
|
||||
def asyncGet(self, screenshotID = None):
|
||||
try:
|
||||
# Make sure the screenshot exists
|
||||
if screenshotID is None or not os.path.isfile(".data/screenshots/{}".format(screenshotID)):
|
||||
raise exceptions.fileNotFoundException(MODULE_NAME, screenshotID)
|
||||
|
||||
# Read screenshot
|
||||
with open(".data/screenshots/{}".format(screenshotID), "rb") as f:
|
||||
data = f.read()
|
||||
|
||||
# Output
|
||||
log.info("Served screenshot {}".format(screenshotID))
|
||||
|
||||
# Display screenshot
|
||||
self.write(data)
|
||||
self.set_header("Content-type", "image/jpg")
|
||||
self.set_header("Content-length", len(data))
|
||||
except exceptions.fileNotFoundException:
|
||||
self.set_status(404)
|
18
handlers/loadTestHandler.py
Normal file
18
handlers/loadTestHandler.py
Normal file
@@ -0,0 +1,18 @@
|
||||
import tornado.gen
|
||||
import tornado.web
|
||||
|
||||
from common.web import requestsManager
|
||||
from objects import glob
|
||||
|
||||
|
||||
class handler(requestsManager.asyncRequestHandler):
|
||||
@tornado.web.asynchronous
|
||||
@tornado.gen.engine
|
||||
def asyncGet(self):
|
||||
if not glob.debug:
|
||||
self.write("Nope")
|
||||
return
|
||||
glob.db.fetchAll("SELECT SQL_NO_CACHE * FROM beatmaps")
|
||||
glob.db.fetchAll("SELECT SQL_NO_CACHE * FROM users")
|
||||
glob.db.fetchAll("SELECT SQL_NO_CACHE * FROM scores")
|
||||
self.write("ibmd")
|
35
handlers/mapsHandler.py
Normal file
35
handlers/mapsHandler.py
Normal file
@@ -0,0 +1,35 @@
|
||||
import tornado.gen
|
||||
import tornado.web
|
||||
|
||||
from common.log import logUtils as log
|
||||
from common.web import requestsManager
|
||||
from constants import exceptions
|
||||
from helpers import osuapiHelper
|
||||
from common.sentry import sentry
|
||||
|
||||
MODULE_NAME = "maps"
|
||||
class handler(requestsManager.asyncRequestHandler):
|
||||
@tornado.web.asynchronous
|
||||
@tornado.gen.engine
|
||||
@sentry.captureTornado
|
||||
def asyncGet(self, fileName = None):
|
||||
try:
|
||||
# Check arguments
|
||||
if fileName is None:
|
||||
raise exceptions.invalidArgumentsException(MODULE_NAME)
|
||||
if fileName == "":
|
||||
raise exceptions.invalidArgumentsException(MODULE_NAME)
|
||||
|
||||
fileNameShort = fileName[:32]+"..." if len(fileName) > 32 else fileName[:-4]
|
||||
log.info("Requested .osu file {}".format(fileNameShort))
|
||||
|
||||
# Get .osu file from osu! server
|
||||
fileContent = osuapiHelper.getOsuFileFromName(fileName)
|
||||
if fileContent is None:
|
||||
# TODO: Sentry capture message here
|
||||
raise exceptions.osuApiFailException(MODULE_NAME)
|
||||
self.write(fileContent)
|
||||
except exceptions.invalidArgumentsException:
|
||||
self.set_status(500)
|
||||
except exceptions.osuApiFailException:
|
||||
self.set_status(500)
|
11
handlers/osuErrorHandler.py
Normal file
11
handlers/osuErrorHandler.py
Normal file
@@ -0,0 +1,11 @@
|
||||
import tornado.gen
|
||||
import tornado.web
|
||||
|
||||
from common.web import requestsManager
|
||||
|
||||
|
||||
class handler(requestsManager.asyncRequestHandler):
|
||||
@tornado.web.asynchronous
|
||||
@tornado.gen.engine
|
||||
def asyncGet(self):
|
||||
self.write("")
|
58
handlers/osuSearchHandler.py
Normal file
58
handlers/osuSearchHandler.py
Normal file
@@ -0,0 +1,58 @@
|
||||
import tornado.gen
|
||||
import tornado.web
|
||||
|
||||
from common.sentry import sentry
|
||||
from common.web import requestsManager
|
||||
from common.web import cheesegull
|
||||
from constants import exceptions
|
||||
from common.log import logUtils as log
|
||||
|
||||
MODULE_NAME = "direct"
|
||||
class handler(requestsManager.asyncRequestHandler):
|
||||
"""
|
||||
Handler for /web/osu-search.php
|
||||
"""
|
||||
@tornado.web.asynchronous
|
||||
@tornado.gen.engine
|
||||
@sentry.captureTornado
|
||||
def asyncGet(self):
|
||||
output = ""
|
||||
try:
|
||||
try:
|
||||
# Get arguments
|
||||
gameMode = self.get_argument("m", None)
|
||||
if gameMode is not None:
|
||||
gameMode = int(gameMode)
|
||||
if gameMode < 0 or gameMode > 3:
|
||||
gameMode = None
|
||||
|
||||
rankedStatus = self.get_argument("r", None)
|
||||
if rankedStatus is not None:
|
||||
rankedStatus = int(rankedStatus)
|
||||
|
||||
query = self.get_argument("q", "")
|
||||
page = int(self.get_argument("p", "0"))
|
||||
if query.lower() in ["newest", "top rated", "most played"]:
|
||||
query = ""
|
||||
except ValueError:
|
||||
raise exceptions.invalidArgumentsException(MODULE_NAME)
|
||||
|
||||
# Get data from cheesegull API
|
||||
log.info("Requested osu!direct search: {}".format(query if query != "" else "index"))
|
||||
searchData = cheesegull.getListing(rankedStatus=cheesegull.directToApiStatus(rankedStatus), page=page * 100, gameMode=gameMode, query=query)
|
||||
if searchData is None or searchData is None:
|
||||
raise exceptions.noAPIDataError()
|
||||
|
||||
# Write output
|
||||
output += "999" if len(searchData) == 100 else str(len(searchData))
|
||||
output += "\n"
|
||||
for beatmapSet in searchData:
|
||||
try:
|
||||
output += cheesegull.toDirect(beatmapSet) + "\r\n"
|
||||
except ValueError:
|
||||
# Invalid cheesegull beatmap (empty beatmapset, cheesegull bug? See Sentry #LETS-00-32)
|
||||
pass
|
||||
except (exceptions.noAPIDataError, exceptions.invalidArgumentsException):
|
||||
output = "0\n"
|
||||
finally:
|
||||
self.write(output)
|
42
handlers/osuSearchSetHandler.py
Normal file
42
handlers/osuSearchSetHandler.py
Normal file
@@ -0,0 +1,42 @@
|
||||
import tornado.gen
|
||||
import tornado.web
|
||||
|
||||
from common.sentry import sentry
|
||||
from common.web import requestsManager
|
||||
from common.web import cheesegull
|
||||
from common.log import logUtils as log
|
||||
from constants import exceptions
|
||||
|
||||
MODULE_NAME = "direct_np"
|
||||
class handler(requestsManager.asyncRequestHandler):
|
||||
"""
|
||||
Handler for /web/osu-search-set.php
|
||||
"""
|
||||
@tornado.web.asynchronous
|
||||
@tornado.gen.engine
|
||||
@sentry.captureTornado
|
||||
def asyncGet(self):
|
||||
output = ""
|
||||
try:
|
||||
# Get data by beatmap id or beatmapset id
|
||||
if "b" in self.request.arguments:
|
||||
_id = self.get_argument("b")
|
||||
data = cheesegull.getBeatmap(_id)
|
||||
elif "s" in self.request.arguments:
|
||||
_id = self.get_argument("s")
|
||||
data = cheesegull.getBeatmapSet(_id)
|
||||
else:
|
||||
raise exceptions.invalidArgumentsException(MODULE_NAME)
|
||||
|
||||
log.info("Requested osu!direct np: {}/{}".format("b" if "b" in self.request.arguments else "s", _id))
|
||||
|
||||
# Make sure cheesegull returned some valid data
|
||||
if data is None or len(data) == 0:
|
||||
raise exceptions.osuApiFailException(MODULE_NAME)
|
||||
|
||||
# Write the response
|
||||
output = cheesegull.toDirectNp(data) + "\r\n"
|
||||
except (exceptions.invalidArgumentsException, exceptions.osuApiFailException, KeyError):
|
||||
output = ""
|
||||
finally:
|
||||
self.write(output)
|
14
handlers/redirectHandler.py
Normal file
14
handlers/redirectHandler.py
Normal file
@@ -0,0 +1,14 @@
|
||||
import tornado.web
|
||||
import tornado.gen
|
||||
|
||||
from common.web import requestsManager
|
||||
|
||||
class handler(requestsManager.asyncRequestHandler):
|
||||
def initialize(self, destination):
|
||||
self.destination = destination
|
||||
|
||||
@tornado.web.asynchronous
|
||||
@tornado.gen.engine
|
||||
def asyncGet(self, args=()):
|
||||
self.set_status(302)
|
||||
self.add_header("location", self.destination.format(args))
|
513
handlers/submitModularHandler.pyx
Normal file
513
handlers/submitModularHandler.pyx
Normal file
@@ -0,0 +1,513 @@
|
||||
import base64
|
||||
import collections
|
||||
import json
|
||||
import sys
|
||||
import threading
|
||||
import traceback
|
||||
from urllib.parse import urlencode
|
||||
|
||||
import requests
|
||||
import tornado.gen
|
||||
import tornado.web
|
||||
import math
|
||||
|
||||
import secret.achievements.utils
|
||||
from common.constants import gameModes
|
||||
from common.constants import mods
|
||||
from common.log import logUtils as log
|
||||
from common.ripple import userUtils
|
||||
from common.ripple import scoreUtils
|
||||
from common.web import requestsManager
|
||||
from constants import exceptions
|
||||
from constants import rankedStatuses
|
||||
from constants.exceptions import ppCalcException
|
||||
from helpers import aeshelper
|
||||
from helpers import replayHelper
|
||||
from helpers import leaderboardHelper
|
||||
from objects import beatmap
|
||||
from objects import glob
|
||||
from objects import score
|
||||
from objects import scoreboard
|
||||
from objects import relaxboard
|
||||
from objects import rxscore
|
||||
from common import generalUtils
|
||||
|
||||
|
||||
MODULE_NAME = "submit_modular"
|
||||
class handler(requestsManager.asyncRequestHandler):
|
||||
"""
|
||||
Handler for /web/osu-submit-modular.php
|
||||
"""
|
||||
@tornado.web.asynchronous
|
||||
@tornado.gen.engine
|
||||
#@sentry.captureTornado
|
||||
def asyncPost(self):
|
||||
try:
|
||||
# Resend the score in case of unhandled exceptions
|
||||
keepSending = True
|
||||
|
||||
# Get request ip
|
||||
ip = self.getRequestIP()
|
||||
|
||||
# Print arguments
|
||||
if glob.debug:
|
||||
requestsManager.printArguments(self)
|
||||
|
||||
# Check arguments
|
||||
if not requestsManager.checkArguments(self.request.arguments, ["score", "iv", "pass"]):
|
||||
raise exceptions.invalidArgumentsException(MODULE_NAME)
|
||||
|
||||
# TODO: Maintenance check
|
||||
|
||||
# Get parameters and IP
|
||||
scoreDataEnc = self.get_argument("score")
|
||||
iv = self.get_argument("iv")
|
||||
password = self.get_argument("pass")
|
||||
ip = self.getRequestIP()
|
||||
|
||||
# Get bmk and bml (notepad hack check)
|
||||
if "bmk" in self.request.arguments and "bml" in self.request.arguments:
|
||||
bmk = self.get_argument("bmk")
|
||||
bml = self.get_argument("bml")
|
||||
else:
|
||||
bmk = None
|
||||
bml = None
|
||||
|
||||
# Get right AES Key
|
||||
if "osuver" in self.request.arguments:
|
||||
aeskey = "osu!-scoreburgr---------{}".format(self.get_argument("osuver"))
|
||||
else:
|
||||
aeskey = "h89f2-890h2h89b34g-h80g134n90133"
|
||||
|
||||
# Get score data
|
||||
log.debug("Decrypting score data...")
|
||||
scoreData = aeshelper.decryptRinjdael(aeskey, iv, scoreDataEnc, True).split(":")
|
||||
username = scoreData[1].strip()
|
||||
|
||||
# Login and ban check
|
||||
userID = userUtils.getID(username)
|
||||
# User exists check
|
||||
if userID == 0:
|
||||
raise exceptions.loginFailedException(MODULE_NAME, userID)
|
||||
# Bancho session/username-pass combo check
|
||||
if not userUtils.checkLogin(userID, password, ip):
|
||||
raise exceptions.loginFailedException(MODULE_NAME, username)
|
||||
# 2FA Check
|
||||
if userUtils.check2FA(userID, ip):
|
||||
raise exceptions.need2FAException(MODULE_NAME, userID, ip)
|
||||
# Generic bancho session check
|
||||
#if not userUtils.checkBanchoSession(userID):
|
||||
# TODO: Ban (see except exceptions.noBanchoSessionException block)
|
||||
# raise exceptions.noBanchoSessionException(MODULE_NAME, username, ip)
|
||||
# Ban check
|
||||
if userUtils.isBanned(userID):
|
||||
raise exceptions.userBannedException(MODULE_NAME, username)
|
||||
# Data length check
|
||||
if len(scoreData) < 16:
|
||||
raise exceptions.invalidArgumentsException(MODULE_NAME)
|
||||
|
||||
# Get restricted
|
||||
restricted = userUtils.isRestricted(userID)
|
||||
|
||||
# Get variables for relax
|
||||
used_mods = int(scoreData[13])
|
||||
isRelaxing = used_mods & 128
|
||||
|
||||
# Create score object and set its data
|
||||
log.info("[{}] {} has submitted a score on {}...".format("RELAX" if isRelaxing else "VANILLA", username, scoreData[0]))
|
||||
s = rxscore.score() if isRelaxing else score.score()
|
||||
s.setDataFromScoreData(scoreData)
|
||||
|
||||
if s.completed == -1:
|
||||
# Duplicated score
|
||||
log.warning("Duplicated score detected, this is normal right after restarting the server")
|
||||
return
|
||||
|
||||
# Set score stuff missing in score data
|
||||
s.playerUserID = userID
|
||||
|
||||
# Get beatmap info
|
||||
beatmapInfo = beatmap.beatmap()
|
||||
beatmapInfo.setDataFromDB(s.fileMd5)
|
||||
|
||||
# Make sure the beatmap is submitted and updated
|
||||
if beatmapInfo.rankedStatus == rankedStatuses.NOT_SUBMITTED or beatmapInfo.rankedStatus == rankedStatuses.NEED_UPDATE or beatmapInfo.rankedStatus == rankedStatuses.UNKNOWN:
|
||||
log.debug("Beatmap is not submitted/outdated/unknown. Score submission aborted.")
|
||||
return
|
||||
|
||||
# increment user playtime
|
||||
length = 0
|
||||
if s.passed:
|
||||
length = userUtils.getBeatmapTime(beatmapInfo.beatmapID)
|
||||
else:
|
||||
length = math.ceil(int(self.get_argument("ft")) / 1000)
|
||||
|
||||
userUtils.incrementPlaytime(userID, s.gameMode, length)
|
||||
# Calculate PP
|
||||
midPPCalcException = None
|
||||
try:
|
||||
s.calculatePP()
|
||||
except Exception as e:
|
||||
# Intercept ALL exceptions and bypass them.
|
||||
# We want to save scores even in case PP calc fails
|
||||
# due to some rippoppai bugs.
|
||||
# I know this is bad, but who cares since I'll rewrite
|
||||
# the scores server again.
|
||||
log.error("Caught an exception in pp calculation, re-raising after saving score in db")
|
||||
s.pp = 0
|
||||
midPPCalcException = e
|
||||
|
||||
# Restrict obvious cheaters™
|
||||
if restricted == False:
|
||||
if isRelaxing: # Relax
|
||||
rxGods = [7340, 2137, 6868, 1215, 15066, 14522, 1325, 5798, 21610, 1254] # Yea yea it's a bad way of doing it, kill yourself - cmyui osu gaming
|
||||
"""
|
||||
CTBLIST = []
|
||||
TAIKOLIST = []
|
||||
"""
|
||||
|
||||
if (s.pp >= 2000 and s.gameMode == gameModes.STD) and userID not in rxGods:
|
||||
userUtils.restrict(userID)
|
||||
userUtils.appendNotes(userID, "Restricted due to too high pp gain ({}pp)".format(s.pp))
|
||||
log.warning("**{}** ({}) has been restricted due to too high pp gain **({}pp)**".format(username, userID, s.pp), "cm")
|
||||
"""
|
||||
elif (s.pp >= 10000 and s.gameMode == gameModes.TAIKO) and userID not in TAIKOLIST:
|
||||
userUtils.restrict(userID)
|
||||
userUtils.appendNotes(userID, "Restricted due to too high pp gain ({}pp)".format(s.pp))
|
||||
log.warning("**{}** ({}) has been restricted due to too high pp gain **({}pp)**".format(username, userID, s.pp), "cm")
|
||||
elif s.pp >= 10000 and (s.gameMode == gameModes.CTB) and userID not in CTBLIST:
|
||||
userUtils.restrict(userID)
|
||||
userUtils.appendNotes(userID, "Restricted due to too high pp gain ({}pp)".format(s.pp))
|
||||
log.warning("**{}** ({}) has been restricted due to too high pp gain **({}pp)**".format(username, userID, s.pp), "cm")
|
||||
"""
|
||||
else: # Vanilla
|
||||
if (s.pp >= 700 and s.gameMode == gameModes.STD):
|
||||
userUtils.restrict(userID)
|
||||
userUtils.appendNotes(userID, "Restricted due to too high pp gain ({}pp)".format(s.pp))
|
||||
log.warning("**{}** ({}) has been restricted due to too high pp gain **({}pp)**".format(username, userID, s.pp), "cm")
|
||||
"""
|
||||
elif (s.pp >= 10000 and s.gameMode == gameModes.TAIKO):
|
||||
userUtils.restrict(userID)
|
||||
userUtils.appendNotes(userID, "Restricted due to too high pp gain ({}pp)".format(s.pp))
|
||||
log.warning("**{}** ({}) has been restricted due to too high pp gain **({}pp)**".format(username, userID, s.pp), "cm")
|
||||
elif s.pp >= 10000 and (s.gameMode == gameModes.CTB):
|
||||
userUtils.restrict(userID)
|
||||
userUtils.appendNotes(userID, "Restricted due to too high pp gain ({}pp)".format(s.pp))
|
||||
log.warning("**{}** ({}) has been restricted due to too high pp gain **({}pp)**".format(username, userID, s.pp), "cm")
|
||||
"""
|
||||
|
||||
# Check notepad hack
|
||||
if bmk is None and bml is None:
|
||||
# No bmk and bml params passed, edited or super old client
|
||||
#log.warning("{} ({}) most likely submitted a score from an edited client or a super old client".format(username, userID), "cm")
|
||||
pass
|
||||
elif bmk != bml and not restricted:
|
||||
# bmk and bml passed and they are different, restrict the user
|
||||
userUtils.restrict(userID)
|
||||
userUtils.appendNotes(userID, "Restricted due to notepad hack")
|
||||
log.warning("**{}** ({}) has been restricted due to notepad hack".format(username, userID), "cm")
|
||||
return
|
||||
|
||||
# Save score in db
|
||||
s.saveScoreInDB()
|
||||
|
||||
# Client anti-cheat flags
|
||||
haxFlags = scoreData[17].count(' ') # 4 is normal, 0 is irregular but inconsistent.
|
||||
if haxFlags != 4 and haxFlags != 0 and s.completed > 1 and restricted == False:
|
||||
|
||||
flagsReadable = generalUtils.calculateFlags(haxFlags, used_mods, s.gameMode)
|
||||
userUtils.appendNotes(userID, "-- has received clientside flags: {} [{}] (cheated score id: {})".format(haxFlags, flagsReadable, s.scoreID))
|
||||
log.warning("**{}** ({}) has received clientside anti cheat flags.\n\nFlags: {}.\n[{}]\n\nScore ID: {scoreID}\nReplay: https://akatsuki.pw/web/replays/{scoreID}".format(username, userID, haxFlags, flagsReadable, scoreID=s.scoreID), "cm")
|
||||
|
||||
if s.score < 0 or s.score > (2 ** 63) - 1:
|
||||
userUtils.ban(userID)
|
||||
userUtils.appendNotes(userID, "Banned due to negative score.")
|
||||
|
||||
# Make sure the score is not memed
|
||||
if s.gameMode == gameModes.MANIA and s.score > 1000000:
|
||||
userUtils.ban(userID)
|
||||
userUtils.appendNotes(userID, "Banned due to mania score > 1000000.")
|
||||
|
||||
# Ci metto la faccia, ci metto la testa e ci metto il mio cuore
|
||||
if ((s.mods & mods.DOUBLETIME) > 0 and (s.mods & mods.HALFTIME) > 0) \
|
||||
or ((s.mods & mods.HARDROCK) > 0 and (s.mods & mods.EASY) > 0) \
|
||||
or ((s.mods & mods.SUDDENDEATH) > 0 and (s.mods & mods.NOFAIL) > 0)\
|
||||
or ((s.mods & mods.RELAX) > 0 and (s.mods & mods.RELAX2) > 0):
|
||||
userUtils.ban(userID)
|
||||
userUtils.appendNotes(userID, "Impossible mod combination ({}).".format(s.mods))
|
||||
|
||||
# NOTE: Process logging was removed from the client starting from 20180322
|
||||
# Save replay for all passed scores
|
||||
# Make sure the score has an id as well (duplicated?, query error?)
|
||||
if s.passed and s.scoreID > 0:
|
||||
if "score" in self.request.files:
|
||||
# Save the replay if it was provided
|
||||
log.debug("Saving replay ({})...".format(s.scoreID))
|
||||
replay = self.request.files["score"][0]["body"]
|
||||
with open(".data/replays/replay_{}.osr".format(s.scoreID), "wb") as f:
|
||||
f.write(replay)
|
||||
|
||||
# Send to cono ALL passed replays, even non high-scores
|
||||
if glob.conf.config["cono"]["enable"]:
|
||||
if isRelaxing:
|
||||
threading.Thread(target=lambda: glob.redis.publish(
|
||||
"cono:analyze", json.dumps({
|
||||
"score_id": s.scoreID,
|
||||
"beatmap_id": beatmapInfo.beatmapID,
|
||||
"user_id": s.playerUserID,
|
||||
"game_mode": s.gameMode,
|
||||
"pp": s.pp,
|
||||
"replay_data": base64.b64encode(
|
||||
replayHelper.rxbuildFullReplay(
|
||||
s.scoreID,
|
||||
rawReplay=self.request.files["score"][0]["body"]
|
||||
)
|
||||
).decode(),
|
||||
})
|
||||
)).start()
|
||||
else:
|
||||
# We run this in a separate thread to avoid slowing down scores submission,
|
||||
# as cono needs a full replay
|
||||
threading.Thread(target=lambda: glob.redis.publish(
|
||||
"cono:analyze", json.dumps({
|
||||
"score_id": s.scoreID,
|
||||
"beatmap_id": beatmapInfo.beatmapID,
|
||||
"user_id": s.playerUserID,
|
||||
"game_mode": s.gameMode,
|
||||
"pp": s.pp,
|
||||
"replay_data": base64.b64encode(
|
||||
replayHelper.buildFullReplay(
|
||||
s.scoreID,
|
||||
rawReplay=self.request.files["score"][0]["body"]
|
||||
)
|
||||
).decode(),
|
||||
})
|
||||
)).start()
|
||||
else:
|
||||
# Restrict if no replay was provided
|
||||
if not restricted:
|
||||
userUtils.restrict(userID)
|
||||
userUtils.appendNotes(userID, "Restricted due to missing replay while submitting a score.")
|
||||
log.warning("**{}** ({}) has been restricted due to not submitting a replay on map {}.".format(
|
||||
username, userID, s.fileMd5
|
||||
), "cm")
|
||||
|
||||
# Update beatmap playcount (and passcount)
|
||||
beatmap.incrementPlaycount(s.fileMd5, s.passed)
|
||||
|
||||
# Let the api know of this score
|
||||
if s.scoreID:
|
||||
glob.redis.publish("api:score_submission", s.scoreID)
|
||||
|
||||
# Re-raise pp calc exception after saving score, cake, replay etc
|
||||
# so Sentry can track it without breaking score submission
|
||||
if midPPCalcException is not None:
|
||||
raise ppCalcException(midPPCalcException)
|
||||
|
||||
# If there was no exception, update stats and build score submitted panel
|
||||
# Get "before" stats for ranking panel (only if passed)
|
||||
if s.passed:
|
||||
# Get stats and rank
|
||||
if isRelaxing:
|
||||
oldUserData = glob.userStatsCache.rxget(userID, s.gameMode)
|
||||
oldRank = userUtils.rxgetGameRank(userID, s.gameMode)
|
||||
else:
|
||||
oldUserData = glob.userStatsCache.get(userID, s.gameMode)
|
||||
oldRank = userUtils.getGameRank(userID, s.gameMode)
|
||||
|
||||
# Try to get oldPersonalBestRank from cache
|
||||
oldPersonalBestRank = glob.personalBestCache.get(userID, s.fileMd5)
|
||||
if oldPersonalBestRank == 0:
|
||||
# oldPersonalBestRank not found in cache, get it from db
|
||||
if isRelaxing:
|
||||
oldScoreboard = relaxboard.scoreboard(username, s.gameMode, beatmapInfo, False)
|
||||
else:
|
||||
oldScoreboard = scoreboard.scoreboard(username, s.gameMode, beatmapInfo, False)
|
||||
|
||||
oldScoreboard.setPersonalBest()
|
||||
oldPersonalBestRank = oldScoreboard.personalBestRank if oldScoreboard.personalBestRank > 0 else 0
|
||||
|
||||
# Always update users stats (total/ranked score, playcount, level, acc and pp)
|
||||
# even if not passed
|
||||
|
||||
log.debug("[{}] Updating {}'s stats...".format("RELAX" if isRelaxing else "VANILLA", username))
|
||||
if isRelaxing:
|
||||
userUtils.rxupdateStats(userID, s)
|
||||
else:
|
||||
userUtils.updateStats(userID, s)
|
||||
|
||||
# Get "after" stats for ranking panel
|
||||
# and to determine if we should update the leaderboard
|
||||
# (only if we passed that song)
|
||||
if s.passed:
|
||||
# Get new stats
|
||||
if isRelaxing:
|
||||
newUserData = userUtils.getRelaxStats(userID, s.gameMode)
|
||||
glob.userStatsCache.rxupdate(userID, s.gameMode, newUserData)
|
||||
else:
|
||||
newUserData = userUtils.getUserStats(userID, s.gameMode)
|
||||
glob.userStatsCache.update(userID, s.gameMode, newUserData)
|
||||
|
||||
# Update leaderboard (global and country) if score/pp has changed
|
||||
if s.completed == 3 and newUserData["pp"] != oldUserData["pp"]:
|
||||
if isRelaxing:
|
||||
leaderboardHelper.rxupdate(userID, newUserData["pp"], s.gameMode)
|
||||
leaderboardHelper.rxupdateCountry(userID, newUserData["pp"], s.gameMode)
|
||||
else:
|
||||
leaderboardHelper.update(userID, newUserData["pp"], s.gameMode)
|
||||
leaderboardHelper.updateCountry(userID, newUserData["pp"], s.gameMode)
|
||||
|
||||
# TODO: Update total hits and max combo
|
||||
# Update latest activity
|
||||
userUtils.updateLatestActivity(userID)
|
||||
|
||||
# IP log
|
||||
userUtils.IPLog(userID, ip)
|
||||
|
||||
# Score submission and stats update done
|
||||
log.debug("Score submission and user stats update done!")
|
||||
|
||||
# Score has been submitted, do not retry sending the score if
|
||||
# there are exceptions while building the ranking panel
|
||||
keepSending = False
|
||||
|
||||
# At the end, check achievements
|
||||
if s.passed:
|
||||
new_achievements = secret.achievements.utils.unlock_achievements(s, beatmapInfo, newUserData)
|
||||
|
||||
# Output ranking panel only if we passed the song
|
||||
# and we got valid beatmap info from db
|
||||
if beatmapInfo is not None and beatmapInfo != False and s.passed:
|
||||
log.debug("Started building ranking panel.")
|
||||
|
||||
|
||||
if isRelaxing: # Relax
|
||||
# Trigger bancho stats cache update
|
||||
glob.redis.publish("peppy:update_rxcached_stats", userID)
|
||||
|
||||
# Get personal best after submitting the score
|
||||
newScoreboard = relaxboard.scoreboard(username, s.gameMode, beatmapInfo, True)
|
||||
|
||||
# Get rank info (current rank, pp/score to next rank, user who is 1 rank above us)
|
||||
rankInfo = leaderboardHelper.rxgetRankInfo(userID, s.gameMode)
|
||||
|
||||
else: # Vanilla
|
||||
# Trigger bancho stats cache update
|
||||
glob.redis.publish("peppy:update_cached_stats", userID)
|
||||
|
||||
# Get personal best after submitting the score
|
||||
newScoreboard = scoreboard.scoreboard(username, s.gameMode, beatmapInfo, True)
|
||||
|
||||
# Get rank info (current rank, pp/score to next rank, user who is 1 rank above us)
|
||||
rankInfo = leaderboardHelper.getRankInfo(userID, s.gameMode)
|
||||
|
||||
# Output dictionary
|
||||
output = collections.OrderedDict()
|
||||
output["beatmapId"] = beatmapInfo.beatmapID
|
||||
output["beatmapSetId"] = beatmapInfo.beatmapSetID
|
||||
output["beatmapPlaycount"] = beatmapInfo.playcount
|
||||
output["beatmapPasscount"] = beatmapInfo.passcount
|
||||
#output["approvedDate"] = "2015-07-09 23:20:14\n"
|
||||
output["approvedDate"] = "\n"
|
||||
output["chartId"] = "overall"
|
||||
output["chartName"] = "Overall Ranking"
|
||||
output["chartEndDate"] = ""
|
||||
output["beatmapRankingBefore"] = oldPersonalBestRank
|
||||
output["beatmapRankingAfter"] = newScoreboard.personalBestRank
|
||||
output["rankedScoreBefore"] = oldUserData["rankedScore"]
|
||||
output["rankedScoreAfter"] = newUserData["rankedScore"]
|
||||
output["totalScoreBefore"] = oldUserData["totalScore"]
|
||||
output["totalScoreAfter"] = newUserData["totalScore"]
|
||||
output["playCountBefore"] = newUserData["playcount"]
|
||||
output["accuracyBefore"] = float(oldUserData["accuracy"])/100
|
||||
output["accuracyAfter"] = float(newUserData["accuracy"])/100
|
||||
output["rankBefore"] = oldRank
|
||||
output["rankAfter"] = rankInfo["currentRank"]
|
||||
output["toNextRank"] = rankInfo["difference"]
|
||||
output["toNextRankUser"] = rankInfo["nextUsername"]
|
||||
output["achievements"] = ""
|
||||
output["achievements-new"] = secret.achievements.utils.achievements_response(new_achievements)
|
||||
output["onlineScoreId"] = s.scoreID
|
||||
|
||||
# Build final string
|
||||
msg = ""
|
||||
for line, val in output.items():
|
||||
msg += "{}:{}".format(line, val)
|
||||
if val != "\n":
|
||||
if (len(output) - 1) != list(output.keys()).index(line):
|
||||
msg += "|"
|
||||
else:
|
||||
msg += "\n"
|
||||
|
||||
# Some debug messages
|
||||
log.debug("Generated output for online ranking screen!")
|
||||
log.debug(msg)
|
||||
|
||||
# Send message to #announce if we're rank #1
|
||||
if newScoreboard.personalBestRank == 1 and s.completed == 3 and not restricted:
|
||||
annmsg = "[{}] [https://akatsuki.pw/u/{} {}] achieved rank #1 on [https://osu.ppy.sh/b/{} {}] ({})".format(
|
||||
"RELAX" if isRelaxing else "VANILLA",
|
||||
userID,
|
||||
username.encode().decode("ASCII", "ignore"),
|
||||
beatmapInfo.beatmapID,
|
||||
beatmapInfo.songName.encode().decode("ASCII", "ignore"),
|
||||
gameModes.getGamemodeFull(s.gameMode)
|
||||
)
|
||||
params = urlencode({"k": glob.conf.config["server"]["apikey"], "to": "#announce", "msg": annmsg})
|
||||
requests.get("{}/api/v1/fokabotMessage?{}".format(glob.conf.config["server"]["banchourl"], params))
|
||||
|
||||
scoreUtils.newFirst(userID, s.scoreID, s.fileMd5, s.gameMode, isRelaxing)
|
||||
|
||||
# Write message to client
|
||||
self.write(msg)
|
||||
else:
|
||||
# No ranking panel, send just "ok"
|
||||
self.write("ok")
|
||||
|
||||
# Send username change request to bancho if needed
|
||||
# (key is deleted bancho-side)
|
||||
newUsername = glob.redis.get("ripple:change_username_pending:{}".format(userID))
|
||||
if newUsername is not None:
|
||||
log.debug("Sending username change request for user {} to Bancho".format(userID))
|
||||
glob.redis.publish("peppy:change_username", json.dumps({
|
||||
"userID": userID,
|
||||
"newUsername": newUsername.decode("utf-8")
|
||||
}))
|
||||
|
||||
# Datadog stats
|
||||
glob.dog.increment(glob.DATADOG_PREFIX+".submitted_scores")
|
||||
except exceptions.invalidArgumentsException:
|
||||
pass
|
||||
except exceptions.loginFailedException:
|
||||
self.write("error: pass")
|
||||
except exceptions.need2FAException:
|
||||
# Send error pass to notify the user
|
||||
# resend the score at regular intervals
|
||||
# for users with memy connection
|
||||
self.set_status(408)
|
||||
self.write("error: 2fa")
|
||||
except exceptions.userBannedException:
|
||||
self.write("error: ban")
|
||||
except exceptions.noBanchoSessionException:
|
||||
# We don't have an active bancho session.
|
||||
# Don't ban the user but tell the client to send the score again.
|
||||
# Once we are sure that this error doesn't get triggered when it
|
||||
# shouldn't (eg: bancho restart), we'll ban users that submit
|
||||
# scores without an active bancho session.
|
||||
# We only log through schiavo atm (see exceptions.py).
|
||||
self.set_status(408)
|
||||
self.write("error: pass")
|
||||
except:
|
||||
# Try except block to avoid more errors
|
||||
try:
|
||||
log.error("Unknown error in {}!\n```{}\n{}```".format(MODULE_NAME, sys.exc_info(), traceback.format_exc()))
|
||||
if glob.sentry:
|
||||
yield tornado.gen.Task(self.captureException, exc_info=True)
|
||||
except:
|
||||
pass
|
||||
|
||||
# Every other exception returns a 408 error (timeout)
|
||||
# This avoids lost scores due to score server crash
|
||||
# because the client will send the score again after some time.
|
||||
if keepSending:
|
||||
self.set_status(408)
|
74
handlers/uploadScreenshotHandler.py
Normal file
74
handlers/uploadScreenshotHandler.py
Normal file
@@ -0,0 +1,74 @@
|
||||
import os
|
||||
import sys
|
||||
import traceback
|
||||
|
||||
import tornado.gen
|
||||
import tornado.web
|
||||
from raven.contrib.tornado import SentryMixin
|
||||
|
||||
from common.log import logUtils as log
|
||||
from common.ripple import userUtils
|
||||
from common.web import requestsManager
|
||||
from constants import exceptions
|
||||
from common import generalUtils
|
||||
from objects import glob
|
||||
from common.sentry import sentry
|
||||
|
||||
MODULE_NAME = "screenshot"
|
||||
class handler(requestsManager.asyncRequestHandler):
|
||||
"""
|
||||
Handler for /web/osu-screenshot.php
|
||||
"""
|
||||
@tornado.web.asynchronous
|
||||
@tornado.gen.engine
|
||||
@sentry.captureTornado
|
||||
def asyncPost(self):
|
||||
try:
|
||||
if glob.debug:
|
||||
requestsManager.printArguments(self)
|
||||
|
||||
# Make sure screenshot file was passed
|
||||
if "ss" not in self.request.files:
|
||||
raise exceptions.invalidArgumentsException(MODULE_NAME)
|
||||
|
||||
# Check user auth because of sneaky people
|
||||
if not requestsManager.checkArguments(self.request.arguments, ["u", "p"]):
|
||||
raise exceptions.invalidArgumentsException(MODULE_NAME)
|
||||
username = self.get_argument("u")
|
||||
password = self.get_argument("p")
|
||||
ip = self.getRequestIP()
|
||||
userID = userUtils.getID(username)
|
||||
if not userUtils.checkLogin(userID, password):
|
||||
raise exceptions.loginFailedException(MODULE_NAME, username)
|
||||
if userUtils.check2FA(userID, ip):
|
||||
raise exceptions.need2FAException(MODULE_NAME, username, ip)
|
||||
|
||||
# Rate limit
|
||||
if glob.redis.get("lets:screenshot:{}".format(userID)) is not None:
|
||||
self.write("no")
|
||||
return
|
||||
glob.redis.set("lets:screenshot:{}".format(userID), 1, 60)
|
||||
|
||||
# Get a random screenshot id
|
||||
found = False
|
||||
screenshotID = ""
|
||||
while not found:
|
||||
screenshotID = generalUtils.randomString(8)
|
||||
if not os.path.isfile(".data/screenshots/{}.jpg".format(screenshotID)):
|
||||
found = True
|
||||
|
||||
# Write screenshot file to .data folder
|
||||
with open(".data/screenshots/{}.jpg".format(screenshotID), "wb") as f:
|
||||
f.write(self.request.files["ss"][0]["body"])
|
||||
|
||||
# Output
|
||||
log.info("New screenshot ({})".format(screenshotID))
|
||||
|
||||
# Return screenshot link
|
||||
self.write("{}/ss/{}.jpg".format(glob.conf.config["server"]["servername"], screenshotID))
|
||||
except exceptions.need2FAException:
|
||||
pass
|
||||
except exceptions.invalidArgumentsException:
|
||||
pass
|
||||
except exceptions.loginFailedException:
|
||||
pass
|
Reference in New Issue
Block a user