Initial commit

This commit is contained in:
Josh
2018-12-09 00:15:56 -05:00
commit aad3c9bb54
125 changed files with 18177 additions and 0 deletions

0
handlers/__init__.py Normal file
View File

View 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
View 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

View 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()

View 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")

View 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
View 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))

View 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>
""")

View 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
View 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")

View 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))

View 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

View 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")

View 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)

View 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
View 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)

View 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("")

View 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)

View 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)

View 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))

View 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)

View 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