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
helpers/__init__.py Normal file
View File

449
helpers/aeshelper.py Normal file
View File

@@ -0,0 +1,449 @@
"""
A pure python (slow) implementation of rijndael with a decent interface
To include -
from rijndael import rijndael
To do a key setup -
r = rijndael(key, block_size = 16)
key must be a string of length 16, 24, or 32
blocksize must be 16, 24, or 32. Default is 16
To use -
ciphertext = r.encrypt(plaintext)
plaintext = r.decrypt(ciphertext)
If any strings are of the wrong length a ValueError is thrown
"""
# ported from the Java reference code by Bram Cohen, April 2001
# this code is public domain, unless someone makes
# an intellectual property claim against the reference
# code, in which case it can be made public domain by
# deleting all the comments and renaming all the variables
import copy
import base64
shifts = [[[0, 0], [1, 3], [2, 2], [3, 1]],
[[0, 0], [1, 5], [2, 4], [3, 3]],
[[0, 0], [1, 7], [3, 5], [4, 4]]]
# [keysize][block_size]
num_rounds = {16: {16: 10, 24: 12, 32: 14}, 24: {16: 12, 24: 12, 32: 14}, 32: {16: 14, 24: 14, 32: 14}}
A = [[1, 1, 1, 1, 1, 0, 0, 0],
[0, 1, 1, 1, 1, 1, 0, 0],
[0, 0, 1, 1, 1, 1, 1, 0],
[0, 0, 0, 1, 1, 1, 1, 1],
[1, 0, 0, 0, 1, 1, 1, 1],
[1, 1, 0, 0, 0, 1, 1, 1],
[1, 1, 1, 0, 0, 0, 1, 1],
[1, 1, 1, 1, 0, 0, 0, 1]]
# produce log and alog tables, needed for multiplying in the
# field GF(2^m) (generator = 3)
alog = [1]
for i in range(255):
j = (alog[-1] << 1) ^ alog[-1]
if j & 0x100 != 0:
j ^= 0x11B
alog.append(j)
log = [0] * 256
for i in range(1, 255):
log[alog[i]] = i
# multiply two elements of GF(2^m)
def mul(a, b):
if a == 0 or b == 0:
return 0
return alog[(log[a & 0xFF] + log[b & 0xFF]) % 255]
# substitution box based on F^{-1}(x)
box = [[0] * 8 for i in range(256)]
box[1][7] = 1
for i in range(2, 256):
j = alog[255 - log[i]]
for t in range(8):
box[i][t] = (j >> (7 - t)) & 0x01
B = [0, 1, 1, 0, 0, 0, 1, 1]
# affine transform: box[i] <- B + A*box[i]
cox = [[0] * 8 for i in range(256)]
for i in range(256):
for t in range(8):
cox[i][t] = B[t]
for j in range(8):
cox[i][t] ^= A[t][j] * box[i][j]
# S-boxes and inverse S-boxes
S = [0] * 256
Si = [0] * 256
for i in range(256):
S[i] = cox[i][0] << 7
for t in range(1, 8):
S[i] ^= cox[i][t] << (7-t)
Si[S[i] & 0xFF] = i
# T-boxes
G = [[2, 1, 1, 3],
[3, 2, 1, 1],
[1, 3, 2, 1],
[1, 1, 3, 2]]
AA = [[0] * 8 for i in range(4)]
for i in range(4):
for j in range(4):
AA[i][j] = G[i][j]
AA[i][i+4] = 1
for i in range(4):
pivot = AA[i][i]
if pivot == 0:
t = i + 1
while AA[t][i] == 0 and t < 4:
t += 1
assert t != 4, 'G matrix must be invertible'
for j in range(8):
AA[i][j], AA[t][j] = AA[t][j], AA[i][j]
pivot = AA[i][i]
for j in range(8):
if AA[i][j] != 0:
AA[i][j] = alog[(255 + log[AA[i][j] & 0xFF] - log[pivot & 0xFF]) % 255]
for t in range(4):
if i != t:
for j in range(i+1, 8):
AA[t][j] ^= mul(AA[i][j], AA[t][i])
AA[t][i] = 0
iG = [[0] * 4 for i in range(4)]
for i in range(4):
for j in range(4):
iG[i][j] = AA[i][j + 4]
def mul4(a, bs):
if a == 0:
return 0
r = 0
for b in bs:
r <<= 8
if b != 0:
r |= mul(a, b)
return r
T1 = []
T2 = []
T3 = []
T4 = []
T5 = []
T6 = []
T7 = []
T8 = []
U1 = []
U2 = []
U3 = []
U4 = []
for t in range(256):
s = S[t]
T1.append(mul4(s, G[0]))
T2.append(mul4(s, G[1]))
T3.append(mul4(s, G[2]))
T4.append(mul4(s, G[3]))
s = Si[t]
T5.append(mul4(s, iG[0]))
T6.append(mul4(s, iG[1]))
T7.append(mul4(s, iG[2]))
T8.append(mul4(s, iG[3]))
U1.append(mul4(t, iG[0]))
U2.append(mul4(t, iG[1]))
U3.append(mul4(t, iG[2]))
U4.append(mul4(t, iG[3]))
# round constants
rcon = [1]
r = 1
for t in range(1, 30):
r = mul(2, r)
rcon.append(r)
del A
del AA
del pivot
del B
del G
del box
del log
del alog
del i
del j
del r
del s
del t
del mul
del mul4
del cox
del iG
class rijndael:
def __init__(self, key, block_size = 16):
if block_size != 16 and block_size != 24 and block_size != 32:
raise ValueError('Invalid block size: ' + str(block_size))
if len(key) != 16 and len(key) != 24 and len(key) != 32:
raise ValueError('Invalid key size: ' + str(len(key)))
self.block_size = block_size
ROUNDS = num_rounds[len(key)][block_size]
BC = block_size // 4
# encryption round keys
Ke = [[0] * BC for i in range(ROUNDS + 1)]
# decryption round keys
Kd = [[0] * BC for i in range(ROUNDS + 1)]
ROUND_KEY_COUNT = (ROUNDS + 1) * BC
KC = len(key) // 4
# copy user material bytes into temporary ints
tk = []
for i in range(0, KC):
tk.append((ord(key[i * 4]) << 24) | (ord(key[i * 4 + 1]) << 16) |
(ord(key[i * 4 + 2]) << 8) | ord(key[i * 4 + 3]))
# copy values into round key arrays
t = 0
j = 0
while j < KC and t < ROUND_KEY_COUNT:
Ke[t // BC][t % BC] = tk[j]
Kd[ROUNDS - (t // BC)][t % BC] = tk[j]
j += 1
t += 1
tt = 0
rconpointer = 0
while t < ROUND_KEY_COUNT:
# extrapolate using phi (the round key evolution function)
tt = tk[KC - 1]
tk[0] ^= (S[(tt >> 16) & 0xFF] & 0xFF) << 24 ^ \
(S[(tt >> 8) & 0xFF] & 0xFF) << 16 ^ \
(S[ tt & 0xFF] & 0xFF) << 8 ^ \
(S[(tt >> 24) & 0xFF] & 0xFF) ^ \
(rcon[rconpointer] & 0xFF) << 24
rconpointer += 1
if KC != 8:
for i in range(1, KC):
tk[i] ^= tk[i-1]
else:
for i in range(1, KC // 2):
tk[i] ^= tk[i-1]
tt = tk[KC // 2 - 1]
tk[KC // 2] ^= (S[ tt & 0xFF] & 0xFF) ^ \
(S[(tt >> 8) & 0xFF] & 0xFF) << 8 ^ \
(S[(tt >> 16) & 0xFF] & 0xFF) << 16 ^ \
(S[(tt >> 24) & 0xFF] & 0xFF) << 24
for i in range(KC // 2 + 1, KC):
tk[i] ^= tk[i-1]
# copy values into round key arrays
j = 0
while j < KC and t < ROUND_KEY_COUNT:
Ke[t // BC][t % BC] = tk[j]
Kd[ROUNDS - (t // BC)][t % BC] = tk[j]
j += 1
t += 1
# inverse MixColumn where needed
for r in range(1, ROUNDS):
for j in range(BC):
tt = Kd[r][j]
Kd[r][j] = U1[(tt >> 24) & 0xFF] ^ \
U2[(tt >> 16) & 0xFF] ^ \
U3[(tt >> 8) & 0xFF] ^ \
U4[ tt & 0xFF]
self.Ke = Ke
self.Kd = Kd
def encrypt(self, plaintext):
if len(plaintext) != self.block_size:
raise ValueError('wrong block length, expected ' + str(self.block_size) + ' got ' + str(len(plaintext)))
Ke = self.Ke
BC = self.block_size // 4
ROUNDS = len(Ke) - 1
if BC == 4:
SC = 0
elif BC == 6:
SC = 1
else:
SC = 2
s1 = shifts[SC][1][0]
s2 = shifts[SC][2][0]
s3 = shifts[SC][3][0]
a = [0] * BC
# temporary work array
t = []
# plaintext to ints + key
for i in range(BC):
t.append((ord(plaintext[i * 4 ]) << 24 |
ord(plaintext[i * 4 + 1]) << 16 |
ord(plaintext[i * 4 + 2]) << 8 |
ord(plaintext[i * 4 + 3]) ) ^ Ke[0][i])
# apply round transforms
for r in range(1, ROUNDS):
for i in range(BC):
a[i] = (T1[(t[ i ] >> 24) & 0xFF] ^
T2[(t[(i + s1) % BC] >> 16) & 0xFF] ^
T3[(t[(i + s2) % BC] >> 8) & 0xFF] ^
T4[ t[(i + s3) % BC] & 0xFF] ) ^ Ke[r][i]
t = copy.copy(a)
# last round is special
result = []
for i in range(BC):
tt = Ke[ROUNDS][i]
result.append((S[(t[ i ] >> 24) & 0xFF] ^ (tt >> 24)) & 0xFF)
result.append((S[(t[(i + s1) % BC] >> 16) & 0xFF] ^ (tt >> 16)) & 0xFF)
result.append((S[(t[(i + s2) % BC] >> 8) & 0xFF] ^ (tt >> 8)) & 0xFF)
result.append((S[ t[(i + s3) % BC] & 0xFF] ^ tt ) & 0xFF)
return ''.join(map(chr, result))
def decrypt(self, ciphertext):
if len(ciphertext) != self.block_size:
raise ValueError('wrong block length, expected ' + str(self.block_size) + ' got ' + str(len(ciphertext)))
Kd = self.Kd
BC = self.block_size // 4
ROUNDS = len(Kd) - 1
if BC == 4:
SC = 0
elif BC == 6:
SC = 1
else:
SC = 2
s1 = shifts[SC][1][1]
s2 = shifts[SC][2][1]
s3 = shifts[SC][3][1]
a = [0] * BC
# temporary work array
t = [0] * BC
# ciphertext to ints + key
for i in range(BC):
t[i] = (ord(ciphertext[i * 4 ]) << 24 |
ord(ciphertext[i * 4 + 1]) << 16 |
ord(ciphertext[i * 4 + 2]) << 8 |
ord(ciphertext[i * 4 + 3]) ) ^ Kd[0][i]
# apply round transforms
for r in range(1, ROUNDS):
for i in range(BC):
a[i] = (T5[(t[ i ] >> 24) & 0xFF] ^
T6[(t[(i + s1) % BC] >> 16) & 0xFF] ^
T7[(t[(i + s2) % BC] >> 8) & 0xFF] ^
T8[ t[(i + s3) % BC] & 0xFF] ) ^ Kd[r][i]
t = copy.copy(a)
# last round is special
result = []
for i in range(BC):
tt = Kd[ROUNDS][i]
result.append((Si[(t[ i ] >> 24) & 0xFF] ^ (tt >> 24)) & 0xFF)
result.append((Si[(t[(i + s1) % BC] >> 16) & 0xFF] ^ (tt >> 16)) & 0xFF)
result.append((Si[(t[(i + s2) % BC] >> 8) & 0xFF] ^ (tt >> 8)) & 0xFF)
result.append((Si[ t[(i + s3) % BC] & 0xFF] ^ tt ) & 0xFF)
return ''.join(map(chr, result))
def encrypt(key, block):
return rijndael(key, len(block)).encrypt(block)
def decrypt(key, block):
return rijndael(key, len(block)).decrypt(block)
class zeropad:
def __init__(self, block_size):
assert 0 < block_size < 256
self.block_size = block_size
def pad(self, pt):
ptlen = len(pt)
padsize = self.block_size - ((ptlen + self.block_size - 1) % self.block_size + 1)
return pt + "\0" * padsize
def unpad(self, ppt):
assert len(ppt) % self.block_size == 0
offset = len(ppt)
if offset == 0:
return ''
end = offset - self.block_size + 1
while offset > end:
offset -= 1
if ppt[offset] != "\0":
return ppt[:offset + 1]
assert False
class cbc:
def __init__(self, padding, cipher, iv):
assert padding.block_size == cipher.block_size
assert len(iv) == cipher.block_size
self.padding = padding
self.cipher = cipher
self.iv = iv
def encrypt(self, pt):
ppt = self.padding.pad(pt)
offset = 0
ct = ''
v = self.iv
while offset < len(ppt):
block = ppt[offset:offset + self.cipher.block_size]
block = self.xorblock(block, v)
block = self.cipher.encrypt(block)
ct += block
offset += self.cipher.block_size
v = block
return ct
def decrypt(self, ct):
assert len(ct) % self.cipher.block_size == 0
ppt = ''
offset = 0
v = self.iv
while offset < len(ct):
block = ct[offset:offset + self.cipher.block_size]
decrypted = self.cipher.decrypt(block)
ppt += self.xorblock(decrypted, v)
offset += self.cipher.block_size
v = block
pt = self.padding.unpad(ppt)
return pt
def xorblock(self, b1, b2):
# sorry, not very Pythonesk
i = 0
r = ''
while i < self.cipher.block_size:
r += chr(ord(b1[i]) ^ ord(b2[i]))
i += 1
return r
def decryptRinjdael(key, iv, data, areBase64 = False):
"""
Where the magic happens
key -- AES key (string)
IV -- IV thing (string)
data -- data to decrypt (string)
areBase64 -- if True, iv and data are passed in base64
"""
if areBase64:
iv = base64.b64decode(iv).decode("latin_1")
data = base64.b64decode(data).decode("latin_1")
r = rijndael(key, 32)
p = zeropad(32)
c = cbc(p, r, iv)
return str(c.decrypt(data))

65
helpers/binaryHelper.py Normal file
View File

@@ -0,0 +1,65 @@
"""That's basically packetHelper.py from pep.py, with some changes to make it work with replay files."""
from constants import dataTypes
import struct
def uleb128Encode(num):
arr = bytearray()
length = 0
if num == 0:
return bytearray(b"\x00")
while num > 0:
arr.append(num & 127)
num >>= 7
if num != 0:
arr[length] |= 128
length+=1
return arr
def packData(__data, __dataType):
data = bytes()
pack = True
packType = "<B"
if __dataType == dataTypes.bbytes:
pack = False
data = __data
elif __dataType == dataTypes.string:
pack = False
if len(__data) == 0:
data += b"\x00"
else:
data += b"\x0B"
data += uleb128Encode(len(__data))
data += str.encode(__data, "latin_1")
elif __dataType == dataTypes.uInt16:
packType = "<H"
elif __dataType == dataTypes.sInt16:
packType = "<h"
elif __dataType == dataTypes.uInt32:
packType = "<L"
elif __dataType == dataTypes.sInt32:
packType = "<l"
elif __dataType == dataTypes.uInt64:
packType = "<Q"
elif __dataType == dataTypes.sInt64:
packType = "<q"
elif __dataType == dataTypes.string:
packType = "<s"
elif __dataType == dataTypes.ffloat:
packType = "<f"
elif __dataType == dataTypes.rawReplay:
pack = False
data += packData(len(__data), dataTypes.uInt32)
data += __data
if pack:
data += struct.pack(packType, __data)
return data
def binaryWrite(structure = None):
if structure is None:
structure = []
packetData = bytes()
for i in structure:
packetData += packData(i[0], i[1])
return packetData

438
helpers/chatHelper.py Normal file
View File

@@ -0,0 +1,438 @@
from common.log import logUtils as log
from common.ripple import userUtils
from constants import exceptions
from constants import messageTemplates
from constants import serverPackets
from events import logoutEvent
from objects import fokabot
from objects import glob
def joinChannel(userID = 0, channel = "", token = None, toIRC = True, force=False):
"""
Join a channel
:param userID: user ID of the user that joins the channel. Optional. token can be used instead.
:param token: user token object of user that joins the channel. Optional. userID can be used instead.
:param channel: channel name
:param toIRC: if True, send this channel join event to IRC. Must be true if joining from bancho. Default: True
:param force: whether to allow game clients to join #spect_ and #multi_ channels
:return: 0 if joined or other IRC code in case of error. Needed only on IRC-side
"""
try:
# Get token if not defined
if token is None:
token = glob.tokens.getTokenFromUserID(userID)
# Make sure the token exists
if token is None:
raise exceptions.userNotFoundException
else:
token = token
# Normal channel, do check stuff
# Make sure the channel exists
if channel not in glob.channels.channels:
raise exceptions.channelUnknownException()
# Make sure a game client is not trying to join a #multi_ or #spect_ channel manually
channelObject = glob.channels.channels[channel]
if channelObject.isSpecial and not token.irc and not force:
raise exceptions.channelUnknownException()
# Add the channel to our joined channel
token.joinChannel(channelObject)
# Send channel joined (IRC)
if glob.irc and not toIRC:
glob.ircServer.banchoJoinChannel(token.username, channel)
# Console output
log.info("{} joined channel {}".format(token.username, channel))
# IRC code return
return 0
except exceptions.channelNoPermissionsException:
log.warning("{} attempted to join channel {}, but they have no read permissions".format(token.username, channel))
return 403
except exceptions.channelUnknownException:
log.warning("{} attempted to join an unknown channel ({})".format(token.username, channel))
return 403
except exceptions.userAlreadyInChannelException:
log.warning("User {} already in channel {}".format(token.username, channel))
return 403
except exceptions.userNotFoundException:
log.warning("User not connected to IRC/Bancho")
return 403 # idk
def partChannel(userID = 0, channel = "", token = None, toIRC = True, kick = False, force=False):
"""
Part a channel
:param userID: user ID of the user that parts the channel. Optional. token can be used instead.
:param token: user token object of user that parts the channel. Optional. userID can be used instead.
:param channel: channel name
:param toIRC: if True, send this channel join event to IRC. Must be true if joining from bancho. Optional. Default: True
:param kick: if True, channel tab will be closed on client. Used when leaving lobby. Optional. Default: False
:param force: whether to allow game clients to part #spect_ and #multi_ channels
:return: 0 if joined or other IRC code in case of error. Needed only on IRC-side
"""
try:
# Make sure the client is not drunk and sends partChannel when closing a PM tab
if not channel.startswith("#"):
return
# Get token if not defined
if token is None:
token = glob.tokens.getTokenFromUserID(userID)
# Make sure the token exists
if token is None:
raise exceptions.userNotFoundException()
else:
token = token
# Determine internal/client name if needed
# (toclient is used clientwise for #multiplayer and #spectator channels)
channelClient = channel
if channel == "#spectator":
if token.spectating is None:
s = userID
else:
s = token.spectatingUserID
channel = "#spect_{}".format(s)
elif channel == "#multiplayer":
channel = "#multi_{}".format(token.matchID)
elif channel.startswith("#spect_"):
channelClient = "#spectator"
elif channel.startswith("#multi_"):
channelClient = "#multiplayer"
# Make sure the channel exists
if channel not in glob.channels.channels:
raise exceptions.channelUnknownException()
# Make sure a game client is not trying to join a #multi_ or #spect_ channel manually
channelObject = glob.channels.channels[channel]
if channelObject.isSpecial and not token.irc and not force:
raise exceptions.channelUnknownException()
# Make sure the user is in the channel
if channel not in token.joinedChannels:
raise exceptions.userNotInChannelException()
# Part channel (token-side and channel-side)
token.partChannel(channelObject)
# Delete temporary channel if everyone left
if "chat/{}".format(channelObject.name) in glob.streams.streams:
if channelObject.temp and len(glob.streams.streams["chat/{}".format(channelObject.name)].clients) - 1 == 0:
glob.channels.removeChannel(channelObject.name)
# Force close tab if needed
# NOTE: Maybe always needed, will check later
if kick:
token.enqueue(serverPackets.channelKicked(channelClient))
# IRC part
if glob.irc and toIRC:
glob.ircServer.banchoPartChannel(token.username, channel)
# Console output
log.info("{} parted channel {} ({})".format(token.username, channel, channelClient))
# Return IRC code
return 0
except exceptions.channelUnknownException:
log.warning("{} attempted to part an unknown channel ({})".format(token.username, channel))
return 403
except exceptions.userNotInChannelException:
log.warning("{} attempted to part {}, but he's not in that channel".format(token.username, channel))
return 442
except exceptions.userNotFoundException:
log.warning("User not connected to IRC/Bancho")
return 442 # idk
def sendMessage(fro = "", to = "", message = "", token = None, toIRC = True):
"""
Send a message to osu!bancho and IRC server
:param fro: sender username. Optional. token can be used instead
:param to: receiver channel (if starts with #) or username
:param message: text of the message
:param token: sender token object. Optional. fro can be used instead
:param toIRC: if True, send the message to IRC. If False, send it to Bancho only. Default: True
:return: 0 if joined or other IRC code in case of error. Needed only on IRC-side
"""
try:
#tokenString = ""
# Get token object if not passed
if token is None:
token = glob.tokens.getTokenFromUsername(fro)
if token is None:
raise exceptions.userNotFoundException()
else:
# token object alredy passed, get its string and its username (fro)
fro = token.username
#tokenString = token.token
# Make sure this is not a tournament client
# if token.tournament:
# raise exceptions.userTournamentException()
# Make sure the user is not in restricted mode
if token.restricted:
raise exceptions.userRestrictedException()
# Make sure the user is not silenced
if token.isSilenced():
raise exceptions.userSilencedException()
# Redirect !report to FokaBot
if message.startswith("!report"):
to = glob.BOT_NAME
# Determine internal name if needed
# (toclient is used clientwise for #multiplayer and #spectator channels)
toClient = to
if to == "#spectator":
if token.spectating is None:
s = token.userID
else:
s = token.spectatingUserID
to = "#spect_{}".format(s)
elif to == "#multiplayer":
to = "#multi_{}".format(token.matchID)
elif to.startswith("#spect_"):
toClient = "#spectator"
elif to.startswith("#multi_"):
toClient = "#multiplayer"
# Make sure the message is valid
if not message.strip():
raise exceptions.invalidArgumentsException()
# Truncate message if > 2048 characters
message = message[:2048]+"..." if len(message) > 2048 else message
# Check for word filters
message = glob.chatFilters.filterMessage(message)
# Build packet bytes
packet = serverPackets.sendMessage(token.username, toClient, message)
# Send the message
isChannel = to.startswith("#")
if isChannel:
# CHANNEL
# Make sure the channel exists
if to not in glob.channels.channels:
raise exceptions.channelUnknownException()
# Make sure the channel is not in moderated mode
if glob.channels.channels[to].moderated and not token.admin:
raise exceptions.channelModeratedException()
# Make sure we are in the channel
if to not in token.joinedChannels:
# I'm too lazy to put and test the correct IRC error code here...
# but IRC is not strict at all so who cares
raise exceptions.channelNoPermissionsException()
# Make sure we have write permissions
if not glob.channels.channels[to].publicWrite and not token.admin:
raise exceptions.channelNoPermissionsException()
# Add message in buffer
token.addMessageInBuffer(to, message)
# Everything seems fine, build recipients list and send packet
glob.streams.broadcast("chat/{}".format(to), packet, but=[token.token])
else:
# USER
# Make sure recipient user is connected
recipientToken = glob.tokens.getTokenFromUsername(to)
if recipientToken is None:
raise exceptions.userNotFoundException()
# Make sure the recipient is not a tournament client
#if recipientToken.tournament:
# raise exceptions.userTournamentException()
# Make sure the recipient is not restricted or we are FokaBot
if recipientToken.restricted and fro.lower() != glob.BOT_NAME:
raise exceptions.userRestrictedException()
# TODO: Make sure the recipient has not disabled PMs for non-friends or he's our friend
# Away check
if recipientToken.awayCheck(token.userID):
sendMessage(to, fro, "\x01ACTION is away: {}\x01".format(recipientToken.awayMessage))
# Check message templates (mods/admins only)
if message in messageTemplates.templates and token.admin:
sendMessage(fro, to, messageTemplates.templates[message])
# Everything seems fine, send packet
recipientToken.enqueue(packet)
# Send the message to IRC
if glob.irc and toIRC:
messageSplitInLines = message.encode("latin-1").decode("utf-8").split("\n")
for line in messageSplitInLines:
if line == messageSplitInLines[:1] and line == "":
continue
glob.ircServer.banchoMessage(fro, to, line)
# Spam protection (ignore FokaBot)
if token.userID > 999:
token.spamProtection()
# Fokabot message
if isChannel or to == glob.BOT_NAME:
fokaMessage = fokabot.fokabotResponse(token.username, to, message)
if fokaMessage:
sendMessage(glob.BOT_NAME, to if isChannel else fro, fokaMessage)
# File and discord logs (public chat only) (to make public only, if to.startswith("#") and not)
if not (message.startswith("\x01ACTION is playing") and to.startswith("#spect_")):
if isChannel:
log.chat("[PUBLIC] {fro} @ {to}: {message}".format(fro=token.username, to=to, message=message.encode("latin-1").decode("utf-8")))
glob.schiavo.sendChatlog("**{fro} @ {to}:** {message}".format(fro=token.username, to=to, message=message.encode("latin-1").decode("utf-8")))
else:
log.pm("[PRIVATE] {fro} @ {to}: {message}".format(fro=token.username, to=to, message=message.encode("latin-1").decode("utf-8")))
glob.schiavo.sendChatlog("**{fro} @ {to}:** {message}".format(fro=token.username, to=to, message=message.encode("latin-1").decode("utf-8")))
return 0
except exceptions.userSilencedException:
token.enqueue(serverPackets.silenceEndTime(token.getSilenceSecondsLeft()))
log.warning("{} tried to send a message during silence".format(token.username))
return 404
except exceptions.channelModeratedException:
log.warning("{} tried to send a message to a channel that is in moderated mode ({})".format(token.username, to))
return 404
except exceptions.channelUnknownException:
log.warning("{} tried to send a message to an unknown channel ({})".format(token.username, to))
return 403
except exceptions.channelNoPermissionsException:
log.warning("{} tried to send a message to channel {}, but they have no write permissions".format(token.username, to))
return 404
except exceptions.userRestrictedException:
log.warning("{} tried to send a message {}, but the recipient is in restricted mode".format(token.username, to))
return 404
except exceptions.userTournamentException:
log.warning("{} tried to send a message {}, but the recipient is a tournament client".format(token.username, to))
return 404
except exceptions.userNotFoundException:
log.warning("User not connected to IRC/Bancho")
return 401
except exceptions.invalidArgumentsException:
log.warning("{} tried to send an invalid message to {}".format(token.username, to))
return 404
""" IRC-Bancho Connect/Disconnect/Join/Part interfaces"""
def fixUsernameForBancho(username):
"""
Convert username from IRC format (without spaces) to Bancho format (with spaces)
:param username: username to convert
:return: converted username
"""
# If there are no spaces or underscores in the name
# return it
if " " not in username and "_" not in username:
return username
# Exact match first
result = glob.db.fetch("SELECT id FROM users WHERE username = %s LIMIT 1", [username])
if result is not None:
return username
# Username not found, replace _ with space
return username.replace("_", " ")
def fixUsernameForIRC(username):
"""
Convert an username from Bancho format to IRC format (underscores instead of spaces)
:param username: username to convert
:return: converted username
"""
return username.replace(" ", "_")
def IRCConnect(username):
"""
Handle IRC login bancho-side.
Add token and broadcast login packet.
:param username: username
:return:
"""
userID = userUtils.getID(username)
if not userID:
log.warning("{} doesn't exist".format(username))
return
glob.tokens.deleteOldTokens(userID)
glob.tokens.addToken(userID, irc=True)
glob.streams.broadcast("main", serverPackets.userPanel(userID))
log.info("{} logged in from IRC".format(username))
def IRCDisconnect(username):
"""
Handle IRC logout bancho-side.
Remove token and broadcast logout packet.
:param username: username
:return:
"""
token = glob.tokens.getTokenFromUsername(username)
if token is None:
log.warning("{} doesn't exist".format(username))
return
logoutEvent.handle(token)
log.info("{} disconnected from IRC".format(username))
def IRCJoinChannel(username, channel):
"""
Handle IRC channel join bancho-side.
:param username: username
:param channel: channel name
:return: IRC return code
"""
userID = userUtils.getID(username)
if not userID:
log.warning("{} doesn't exist".format(username))
return
# NOTE: This should have also `toIRC` = False` tho,
# since we send JOIN message later on ircserver.py.
# Will test this later
return joinChannel(userID, channel)
def IRCPartChannel(username, channel):
"""
Handle IRC channel part bancho-side.
:param username: username
:param channel: channel name
:return: IRC return code
"""
userID = userUtils.getID(username)
if not userID:
log.warning("{} doesn't exist".format(username))
return
return partChannel(userID, channel)
def IRCAway(username, message):
"""
Handle IRC away command bancho-side.
:param username:
:param message: away message
:return: IRC return code
"""
userID = userUtils.getID(username)
if not userID:
log.warning("{} doesn't exist".format(username))
return
glob.tokens.getTokenFromUserID(userID).awayMessage = message
return 305 if message == "" else 306

148
helpers/config.py Normal file
View File

@@ -0,0 +1,148 @@
import os
import configparser
class config:
"""
config.ini object
config -- list with ini data
default -- if true, we have generated a default config.ini
"""
config = configparser.ConfigParser()
fileName = "" # config filename
default = True
# Check if config.ini exists and load/generate it
def __init__(self, __file):
"""
Initialize a config object
__file -- filename
"""
self.fileName = __file
if os.path.isfile(self.fileName):
# config.ini found, load it
self.config.read(self.fileName)
self.default = False
else:
# config.ini not found, generate a default one
self.generateDefaultConfig()
self.default = True
# Check if config.ini has all needed the keys
def checkConfig(self):
"""
Check if this config has the required keys
return -- True if valid, False if not
"""
try:
# Try to get all the required keys
self.config.get("db","host")
self.config.get("db","username")
self.config.get("db","password")
self.config.get("db","database")
self.config.get("db","workers")
self.config.get("redis","host")
self.config.get("redis","port")
self.config.get("redis","database")
self.config.get("redis","password")
self.config.get("server","host")
self.config.get("server","port")
self.config.get("server", "debug")
self.config.get("server", "beatmapcacheexpire")
self.config.get("server", "serverurl")
self.config.get("server", "banchourl")
self.config.get("server", "threads")
self.config.get("server", "apikey")
self.config.get("sentry","enable")
self.config.get("sentry","dsn")
self.config.get("datadog", "enable")
self.config.get("datadog", "apikey")
self.config.get("datadog", "appkey")
self.config.get("osuapi","enable")
self.config.get("osuapi","apiurl")
self.config.get("osuapi","apikey")
self.config.get("cheesegull", "apiurl")
self.config.get("discord","enable")
self.config.get("discord","boturl")
self.config.get("discord", "devgroup")
self.config.get("discord", "secretwebhook")
self.config.get("cono", "enable")
return True
except:
return False
# Generate a default config.ini
def generateDefaultConfig(self):
"""Open and set default keys for that config file"""
# Open config.ini in write mode
f = open(self.fileName, "w")
# Set keys to config object
self.config.add_section("db")
self.config.set("db", "host", "localhost")
self.config.set("db", "username", "root")
self.config.set("db", "password", "")
self.config.set("db", "database", "ripple")
self.config.set("db", "workers", "16")
self.config.add_section("redis")
self.config.set("redis", "host", "localhost")
self.config.set("redis", "port", "6379")
self.config.set("redis", "database", "0")
self.config.set("redis", "password", "")
self.config.add_section("server")
self.config.set("server", "host", "0.0.0.0")
self.config.set("server", "port", "5002")
self.config.set("server", "debug", "False")
self.config.set("server", "beatmapcacheexpire", "86400")
self.config.set("server", "serverurl", "http://127.0.0.1:5002")
self.config.set("server", "banchourl", "http://127.0.0.1:5001")
self.config.set("server", "threads", "16")
self.config.set("server", "apikey", "changeme")
self.config.add_section("sentry")
self.config.set("sentry", "enable", "False")
self.config.set("sentry", "dsn", "")
self.config.add_section("datadog")
self.config.set("datadog", "enable", "False")
self.config.set("datadog", "apikey", "")
self.config.set("datadog", "appkey", "")
self.config.add_section("osuapi")
self.config.set("osuapi", "enable", "True")
self.config.set("osuapi", "apiurl", "https://osu.ppy.sh")
self.config.set("osuapi", "apikey", "YOUR_OSU_API_KEY_HERE")
self.config.add_section("cheesegull")
self.config.set("cheesegull", "apiurl", "http://cheesegu.ll/api")
self.config.add_section("discord")
self.config.set("discord", "enable", "False")
self.config.set("discord", "boturl", "")
self.config.set("discord", "devgroup", "")
self.config.set("discord", "secretwebhook", "")
self.config.add_section("cono")
self.config.set("cono", "enable", "False")
# Write ini to file and close
self.config.write(f)
f.close()

99
helpers/consoleHelper.py Normal file
View File

@@ -0,0 +1,99 @@
"""Some console related functions"""
from common.constants import bcolors
from objects import glob
def printServerStartHeader(asciiArt):
"""
Print server start header with optional ascii art
asciiArt -- if True, will print ascii art too
"""
if asciiArt:
printColored(" ( ( ", bcolors.YELLOW)
printColored(" )\\ ) * ) )\\ ) ", bcolors.YELLOW)
printColored("(()/( ( ` ) /((()/( ", bcolors.YELLOW)
printColored(" /(_)) )\\ ( )(_))/(_)) ", bcolors.YELLOW)
printColored("(_)) ((_) (_(_())(_)) ", bcolors.YELLOW)
printColored("| | | __||_ _|/ __| ", bcolors.GREEN)
printColored("| |__ | _| | | \\__ \\ ", bcolors.GREEN)
printColored("|____||___| |_| |___/ \n", bcolors.GREEN)
printColored("> Welcome to the Latest Essential Tatoe Server v{}".format(glob.VERSION), bcolors.GREEN)
printColored("> Made by the Ripple and Akatsuki teams", bcolors.GREEN)
printColored("> {}https://github.com/cmyui/lets".format(bcolors.UNDERLINE), bcolors.GREEN)
printColored("> Press CTRL+C to exit\n", bcolors.GREEN)
def printNoNl(string):
"""
Print string without new line at the end
string -- string to print
"""
print(string, end="")
def printColored(string, color):
"""
Print colored string
string -- string to print
color -- see bcolors.py
"""
print("{}{}{}".format(color, string, bcolors.ENDC))
def printError():
"""Print error text FOR LOADING"""
printColored("Error", bcolors.RED)
def printDone():
"""Print error text FOR LOADING"""
printColored("Done", bcolors.GREEN)
def printWarning():
"""Print error text FOR LOADING"""
printColored("Warning", bcolors.YELLOW)
def printGetScoresMessage(message):
printColored("[get_scores] {}".format(message), bcolors.PINK)
def printSubmitModularMessage(message):
printColored("[submit_modular] {}".format(message), bcolors.YELLOW)
def printBanchoConnectMessage(message):
printColored("[bancho_connect] {}".format(message), bcolors.YELLOW)
def printGetReplayMessage(message):
printColored("[get_replay] {}".format(message), bcolors.PINK)
def printMapsMessage(message):
printColored("[maps] {}".format(message), bcolors.PINK)
def printRippMessage(message):
printColored("[ripp] {}".format(message), bcolors.GREEN)
# def printRippoppaiMessage(message):
# printColored("[rippoppai] {}".format(message), bcolors.GREEN)
def printWifiPianoMessage(message):
printColored("[wifipiano] {}".format(message), bcolors.GREEN)
def printDebugMessage(message):
printColored("[debug] {}".format(message), bcolors.BLUE)
def printScreenshotsMessage(message):
printColored("[screenshots] {}".format(message), bcolors.YELLOW)
def printApiMessage(module, message):
printColored("[{}] {}".format(module, message), bcolors.GREEN)

View File

@@ -0,0 +1,17 @@
import sys
import traceback
from functools import wraps
from common.log import logUtils as log
def trackExceptions(moduleName=""):
def _trackExceptions(func):
def _decorator(request, *args, **kwargs):
try:
response = func(request, *args, **kwargs)
return response
except:
log.error("Unknown error{}!\n```\n{}\n{}```".format(" in "+moduleName if moduleName != "" else "", sys.exc_info(), traceback.format_exc()), True)
return wraps(func)(_decorator)
return _trackExceptions

View File

@@ -0,0 +1,131 @@
from common.log import logUtils as log
from common.ripple import scoreUtils
from objects import glob
from common.ripple import userUtils
def rxgetRankInfo(userID, gameMode):
"""
Get userID's current rank, user above us and pp/score difference
:param userID: user
:param gameMode: gameMode number
:return: {"nextUsername": "", "difference": 0, "currentRank": 0}
"""
data = {"nextUsername": "", "difference": 0, "currentRank": 0}
k = "ripple:relaxboard:{}".format(scoreUtils.readableGameMode(gameMode))
position = userUtils.rxgetGameRank(userID, gameMode) - 1
log.debug("Our position is {}".format(position))
if position is not None and position > 0:
aboveUs = glob.redis.zrevrange(k, position - 1, position)
log.debug("{} is above us".format(aboveUs))
if aboveUs is not None and len(aboveUs) > 0 and aboveUs[0].isdigit():
# Get our rank, next rank username and pp/score difference
myScore = glob.redis.zscore(k, userID)
otherScore = glob.redis.zscore(k, aboveUs[0])
nextUsername = userUtils.getUsername(aboveUs[0])
if nextUsername is not None and myScore is not None and otherScore is not None:
data["nextUsername"] = nextUsername
data["difference"] = int(myScore) - int(otherScore)
else:
position = 0
data["currentRank"] = position + 1
return data
def getRankInfo(userID, gameMode):
"""
Get userID's current rank, user above us and pp/score difference
:param userID: user
:param gameMode: gameMode number
:return: {"nextUsername": "", "difference": 0, "currentRank": 0}
"""
data = {"nextUsername": "", "difference": 0, "currentRank": 0}
k = "ripple:leaderboard:{}".format(scoreUtils.readableGameMode(gameMode))
position = userUtils.getGameRank(userID, gameMode) - 1
log.debug("Our position is {}".format(position))
if position is not None and position > 0:
aboveUs = glob.redis.zrevrange(k, position - 1, position)
log.debug("{} is above us".format(aboveUs))
if aboveUs is not None and len(aboveUs) > 0 and aboveUs[0].isdigit():
# Get our rank, next rank username and pp/score difference
myScore = glob.redis.zscore(k, userID)
otherScore = glob.redis.zscore(k, aboveUs[0])
nextUsername = userUtils.getUsername(aboveUs[0])
if nextUsername is not None and myScore is not None and otherScore is not None:
data["nextUsername"] = nextUsername
data["difference"] = int(myScore) - int(otherScore)
else:
position = 0
data["currentRank"] = position + 1
return data
def rxupdate(userID, newScore, gameMode):
"""
Update gamemode's leaderboard.
Doesn't do anything if userID is banned/restricted.
:param userID: user
:param newScore: new score or pp
:param gameMode: gameMode number
"""
if userUtils.isAllowed(userID):
log.debug("Updating relaxboard...")
glob.redis.zadd("ripple:relaxboard:{}".format(scoreUtils.readableGameMode(gameMode)), str(userID), str(newScore))
else:
log.debug("Relaxboard update for user {} skipped (not allowed)".format(userID))
def update(userID, newScore, gameMode):
"""
Update gamemode's leaderboard.
Doesn't do anything if userID is banned/restricted.
:param userID: user
:param newScore: new score or pp
:param gameMode: gameMode number
"""
if userUtils.isAllowed(userID):
log.debug("Updating leaderboard...")
glob.redis.zadd("ripple:leaderboard:{}".format(scoreUtils.readableGameMode(gameMode)), str(userID), str(newScore))
else:
log.debug("Leaderboard update for user {} skipped (not allowed)".format(userID))
def rxupdateCountry(userID, newScore, gameMode):
"""
Update gamemode's country leaderboard.
Doesn't do anything if userID is banned/restricted.
:param userID: user, country is determined by the user
:param newScore: new score or pp
:param gameMode: gameMode number
:return:
"""
if userUtils.isAllowed(userID):
country = userUtils.getCountry(userID)
if country is not None and len(country) > 0 and country.lower() != "xx":
log.debug("Updating {} country relaxboard...".format(country))
k = "ripple:relaxboard:{}:{}".format(scoreUtils.readableGameMode(gameMode), country.lower())
glob.redis.zadd(k, str(userID), str(newScore))
else:
log.debug("Country relaxboard update for user {} skipped (not allowed)".format(userID))
def updateCountry(userID, newScore, gameMode):
"""
Update gamemode's country leaderboard.
Doesn't do anything if userID is banned/restricted.
:param userID: user, country is determined by the user
:param newScore: new score or pp
:param gameMode: gameMode number
:return:
"""
if userUtils.isAllowed(userID):
country = userUtils.getCountry(userID)
if country is not None and len(country) > 0 and country.lower() != "xx":
log.debug("Updating {} country leaderboard...".format(country))
k = "ripple:leaderboard:{}:{}".format(scoreUtils.readableGameMode(gameMode), country.lower())
glob.redis.zadd(k, str(userID), str(newScore))
else:
log.debug("Country leaderboard update for user {} skipped (not allowed)".format(userID))

51
helpers/levbodHelper.py Normal file
View File

@@ -0,0 +1,51 @@
import requests
import json
from constants import exceptions
from objects import glob
def levbodRequest(handler, params=None):
if params is None:
params = {}
result = requests.get("{}/{}".format(glob.conf.config["levbod"]["url"], handler), params=params)
try:
data = json.loads(result.text)
except (json.JSONDecodeError, ValueError, requests.RequestException, KeyError, exceptions.noAPIDataError):
return None
if result.status_code != 200 or "data" not in data:
return None
return data["data"]
def getListing(rankedStatus=4, page=0, gameMode=-1, query=""):
return levbodRequest("listing", {
"mode": gameMode,
"status": rankedStatus,
"query": query,
"page": page,
})
def getBeatmapSet(id):
return levbodRequest("beatmapset", {
"id": id
})
def getBeatmap(id):
return levbodRequest("beatmap", {
"id": id
})
def levbodToDirect(data):
s = "{beatmapset_id}.osz|{artist}|{title}|{creator}|{ranked_status}|10.00|0|{beatmapset_id}|".format(**data)
if len(data["beatmaps"]) > 0:
s += "{}|0|0|0||".format(data["beatmaps"][0]["beatmap_id"])
for i in data["beatmaps"]:
s += "{difficulty_name}@{game_mode},".format(**i)
s = s.strip(",")
s += "|"
return s
def levbodToDirectNp(data):
return "{beatmapset_id}.osz|{artist}|{title}|{creator}|{ranked_status}|10.00|0|{beatmapset_id}|{beatmapset_id}|0|0|0|".format(**data)

56
helpers/mapsHelper.py Normal file
View File

@@ -0,0 +1,56 @@
import os
from common import generalUtils
from common.log import logUtils as log
from constants import exceptions
from helpers import osuapiHelper
def isBeatmap(fileName=None, content=None):
if fileName is not None:
with open(fileName, "rb") as f:
firstLine = f.readline().decode("utf-8-sig").strip()
elif content is not None:
try:
firstLine = content.decode("utf-8-sig").split("\n")[0].strip()
except IndexError:
return False
else:
raise ValueError("Either `fileName` or `content` must be provided.")
return firstLine.lower().startswith("osu file format v")
def cacheMap(mapFile, _beatmap):
# Check if we have to download the .osu file
download = False
if not os.path.isfile(mapFile):
# .osu file doesn't exist. We must download it
download = True
else:
# File exists, check md5
if generalUtils.fileMd5(mapFile) != _beatmap.fileMD5 or not isBeatmap(mapFile):
# MD5 don't match, redownload .osu file
download = True
# Download .osu file if needed
if download:
log.debug("maps ~> Downloading {} osu file".format(_beatmap.beatmapID))
# Get .osu file from osu servers
fileContent = osuapiHelper.getOsuFileFromID(_beatmap.beatmapID)
# Make sure osu servers returned something
if fileContent is None or not isBeatmap(content=fileContent):
raise exceptions.osuApiFailException("maps")
# Delete old .osu file if it exists
if os.path.isfile(mapFile):
os.remove(mapFile)
# Save .osu file
with open(mapFile, "wb+") as f:
f.write(fileContent)
else:
# Map file is already in folder
log.debug("maps ~> Beatmap found in cache!")
def cachedMapPath(beatmap_id):
return ".data/beatmaps/{}.osu".format(beatmap_id)

83
helpers/osuapiHelper.py Normal file
View File

@@ -0,0 +1,83 @@
import json
from urllib.parse import quote
import requests
from common.log import logUtils as log
from common import generalUtils
from objects import glob
from constants import exceptions
def osuApiRequest(request, params, getFirst=True):
"""
Send a request to osu!api.
request -- request type, string (es: get_beatmaps)
params -- GET parameters, without api key or trailing ?/& (es: h=a5b99395a42bd55bc5eb1d2411cbdf8b&limit=10)
return -- dictionary with json response if success, None if failed or empty response.
"""
# Make sure osuapi is enabled
if not generalUtils.stringToBool(glob.conf.config["osuapi"]["enable"]):
log.warning("osu!api is disabled")
return None
# Api request
resp = None
try:
finalURL = "{}/api/{}?k={}&{}".format(glob.conf.config["osuapi"]["apiurl"], request, glob.conf.config["osuapi"]["apikey"], params)
log.debug(finalURL)
resp = requests.get(finalURL, timeout=5).text
data = json.loads(resp)
if getFirst:
if len(data) >= 1:
resp = data[0]
else:
resp = None
else:
resp = data
finally:
glob.dog.increment(glob.DATADOG_PREFIX+".osu_api.requests")
log.debug(str(resp).encode("utf-8"))
return resp
def getOsuFileFromName(fileName):
"""
Send a request to osu! servers to download a .osu file from file name
Used to update beatmaps
fileName -- .osu file name to download
return -- .osu file content if success, None if failed
"""
# Make sure osuapi is enabled
if not generalUtils.stringToBool(glob.conf.config["osuapi"]["enable"]):
log.warning("osuapi is disabled")
return None
response = None
try:
URL = "{}/web/maps/{}".format(glob.conf.config["osuapi"]["apiurl"], quote(fileName))
req = requests.get(URL, timeout=20)
req.encoding = "utf-8"
response = req.content
finally:
glob.dog.increment(glob.DATADOG_PREFIX+".osu_api.osu_file_requests")
return response
def getOsuFileFromID(beatmapID):
"""
Send a request to osu! servers to download a .osu file from beatmap ID
Used to get .osu files for oppai
beatmapID -- ID of beatmap (not beatmapset) to download
return -- .osu file content if success, None if failed
"""
# Make sure osuapi is enabled
if not generalUtils.stringToBool(glob.conf.config["osuapi"]["enable"]):
log.warning("osuapi is disabled")
return None
response = None
try:
URL = "{}/osu/{}".format(glob.conf.config["osuapi"]["apiurl"], beatmapID)
response = requests.get(URL, timeout=20).content
finally:
glob.dog.increment(glob.DATADOG_PREFIX+".osu_api.osu_file_requests")
return response

134
helpers/replayHelper.py Normal file
View File

@@ -0,0 +1,134 @@
import os
from common import generalUtils
from constants import exceptions, dataTypes
from helpers import binaryHelper
from objects import glob
def rxbuildFullReplay(scoreID=None, scoreData=None, rawReplay=None):
if all(v is None for v in (scoreID, scoreData)) or all(v is not None for v in (scoreID, scoreData)):
raise AttributeError("Either scoreID or scoreData must be provided, not neither or both")
if scoreData is None:
scoreData = glob.db.fetch(
"SELECT scores_relax.*, users.username FROM scores_relax LEFT JOIN users ON scores_relax.userid = users.id "
"WHERE scores_relax.id = %s",
[scoreID]
)
else:
scoreID = scoreData["id"]
if scoreData is None or scoreID is None:
raise exceptions.scoreNotFoundError()
if rawReplay is None:
# Make sure raw replay exists
fileName = ".data/replays/replay_{}.osr".format(scoreID)
if not os.path.isfile(fileName):
raise FileNotFoundError()
# Read raw replay
with open(fileName, "rb") as f:
rawReplay = f.read()
# Calculate missing replay data
rank = generalUtils.getRank(int(scoreData["play_mode"]), int(scoreData["mods"]), int(scoreData["accuracy"]),
int(scoreData["300_count"]), int(scoreData["100_count"]), int(scoreData["50_count"]),
int(scoreData["misses_count"]))
magicHash = generalUtils.stringMd5(
"{}p{}o{}o{}t{}a{}r{}e{}y{}o{}u{}{}{}".format(int(scoreData["100_count"]) + int(scoreData["300_count"]),
scoreData["50_count"], scoreData["gekis_count"],
scoreData["katus_count"], scoreData["misses_count"],
scoreData["beatmap_md5"], scoreData["max_combo"],
"True" if int(scoreData["full_combo"]) == 1 else "False",
scoreData["username"], scoreData["score"], rank,
scoreData["mods"], "True"))
# Add headers (convert to full replay)
fullReplay = binaryHelper.binaryWrite([
[scoreData["play_mode"], dataTypes.byte],
[20150414, dataTypes.uInt32],
[scoreData["beatmap_md5"], dataTypes.string],
[scoreData["username"], dataTypes.string],
[magicHash, dataTypes.string],
[scoreData["300_count"], dataTypes.uInt16],
[scoreData["100_count"], dataTypes.uInt16],
[scoreData["50_count"], dataTypes.uInt16],
[scoreData["gekis_count"], dataTypes.uInt16],
[scoreData["katus_count"], dataTypes.uInt16],
[scoreData["misses_count"], dataTypes.uInt16],
[scoreData["score"], dataTypes.uInt32],
[scoreData["max_combo"], dataTypes.uInt16],
[scoreData["full_combo"], dataTypes.byte],
[scoreData["mods"], dataTypes.uInt32],
[0, dataTypes.byte],
[0, dataTypes.uInt64],
[rawReplay, dataTypes.rawReplay],
[0, dataTypes.uInt32],
[0, dataTypes.uInt32],
])
# Return full replay
return fullReplay
def buildFullReplay(scoreID=None, scoreData=None, rawReplay=None):
if all(v is None for v in (scoreID, scoreData)) or all(v is not None for v in (scoreID, scoreData)):
raise AttributeError("Either scoreID or scoreData must be provided, not neither or both")
if scoreData is None:
scoreData = glob.db.fetch(
"SELECT scores.*, users.username FROM scores LEFT JOIN users ON scores.userid = users.id "
"WHERE scores.id = %s",
[scoreID]
)
else:
scoreID = scoreData["id"]
if scoreData is None or scoreID is None:
raise exceptions.scoreNotFoundError()
if rawReplay is None:
# Make sure raw replay exists
fileName = ".data/replays/replay_{}.osr".format(scoreID)
if not os.path.isfile(fileName):
raise FileNotFoundError()
# Read raw replay
with open(fileName, "rb") as f:
rawReplay = f.read()
# Calculate missing replay data
rank = generalUtils.getRank(int(scoreData["play_mode"]), int(scoreData["mods"]), int(scoreData["accuracy"]),
int(scoreData["300_count"]), int(scoreData["100_count"]), int(scoreData["50_count"]),
int(scoreData["misses_count"]))
magicHash = generalUtils.stringMd5(
"{}p{}o{}o{}t{}a{}r{}e{}y{}o{}u{}{}{}".format(int(scoreData["100_count"]) + int(scoreData["300_count"]),
scoreData["50_count"], scoreData["gekis_count"],
scoreData["katus_count"], scoreData["misses_count"],
scoreData["beatmap_md5"], scoreData["max_combo"],
"True" if int(scoreData["full_combo"]) == 1 else "False",
scoreData["username"], scoreData["score"], rank,
scoreData["mods"], "True"))
# Add headers (convert to full replay)
fullReplay = binaryHelper.binaryWrite([
[scoreData["play_mode"], dataTypes.byte],
[20150414, dataTypes.uInt32],
[scoreData["beatmap_md5"], dataTypes.string],
[scoreData["username"], dataTypes.string],
[magicHash, dataTypes.string],
[scoreData["300_count"], dataTypes.uInt16],
[scoreData["100_count"], dataTypes.uInt16],
[scoreData["50_count"], dataTypes.uInt16],
[scoreData["gekis_count"], dataTypes.uInt16],
[scoreData["katus_count"], dataTypes.uInt16],
[scoreData["misses_count"], dataTypes.uInt16],
[scoreData["score"], dataTypes.uInt32],
[scoreData["max_combo"], dataTypes.uInt16],
[scoreData["full_combo"], dataTypes.byte],
[scoreData["mods"], dataTypes.uInt32],
[0, dataTypes.byte],
[0, dataTypes.uInt64],
[rawReplay, dataTypes.rawReplay],
[0, dataTypes.uInt32],
[0, dataTypes.uInt32],
])
# Return full replay
return fullReplay