Add submodules

This commit is contained in:
Nyo
2016-10-02 22:48:14 +02:00
parent 40264ceffe
commit 88c80a4080
55 changed files with 405 additions and 1829 deletions

View File

@@ -1,12 +1,12 @@
from objects import glob
from helpers import logHelper as log
from common.log import logUtils as log
from common.ripple import userUtils
from constants import exceptions
from constants import serverPackets
from objects import fokabot
from helpers import discordBotHelper
from helpers import userHelper
from events import logoutEvent
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):
"""
@@ -272,7 +272,7 @@ def sendMessage(fro = "", to = "", message = "", token = None, toIRC = True):
# File and discord logs (public chat only)
if to.startswith("#"):
log.chat("{fro} @ {to}: {message}".format(fro=username, to=to, message=str(message.encode("utf-8"))))
discordBotHelper.sendChatlog("**{fro} @ {to}:** {message}".format(fro=username, to=to, message=str(message.encode("utf-8"))[2:-1]))
glob.schiavo.sendChatlog("**{fro} @ {to}:** {message}".format(fro=username, to=to, message=str(message.encode("utf-8"))[2:-1]))
return 0
except exceptions.userSilencedException:
token.enqueue(serverPackets.silenceEndTime(token.getSilenceSecondsLeft()))
@@ -314,7 +314,7 @@ def fixUsernameForIRC(username):
return username.replace(" ", "_")
def IRCConnect(username):
userID = userHelper.getID(username)
userID = userUtils.getID(username)
if not userID:
log.warning("{} doesn't exist".format(username))
return
@@ -332,7 +332,7 @@ def IRCDisconnect(username):
log.info("{} disconnected from IRC".format(username))
def IRCJoinChannel(username, channel):
userID = userHelper.getID(username)
userID = userUtils.getID(username)
if not userID:
log.warning("{} doesn't exist".format(username))
return
@@ -342,7 +342,7 @@ def IRCJoinChannel(username, channel):
return joinChannel(userID, channel)
def IRCPartChannel(username, channel):
userID = userHelper.getID(username)
userID = userUtils.getID(username)
if not userID:
log.warning("{} doesn't exist".format(username))
return

View File

@@ -1,4 +1,4 @@
from constants import bcolors
from common.constants import bcolors
from objects import glob
def printServerStartHeader(asciiArt):
@@ -28,7 +28,7 @@ def printServerStartHeader(asciiArt):
printColored("> Welcome to pep.py osu!bancho server v{}".format(glob.VERSION), bcolors.GREEN)
printColored("> Made by the Ripple team", bcolors.GREEN)
printColored("> {}https://git.zxq.co/ripple/pep.py".format(bcolors.UNDERLINE), bcolors.GREEN)
printColored("> Press CTRL+C to exit\n",bcolors.GREEN)
printColored("> Press CTRL+C to exit\n", bcolors.GREEN)
def printNoNl(string):
"""

View File

@@ -1,222 +0,0 @@
import queue
import MySQLdb
from helpers import logHelper as log
class worker():
"""
A single MySQL worker
"""
def __init__(self, connection, temporary=False):
"""
Initialize a MySQL worker
:param connection: database connection object
:param temporary: if True, this worker will be flagged as temporary
"""
self.connection = connection
self.temporary = temporary
log.debug("Created MySQL worker. Temporary: {}".format(self.temporary))
def ping(self):
"""
Ping MySQL server using this worker.
:return: True if connected, False if error occured.
"""
try:
self.connection.cursor(MySQLdb.cursors.DictCursor).execute("SELECT 1+1")
return True
except:
return False
def __del__(self):
"""
Close connection to the server
:return:
"""
self.connection.close()
log.debug("Destroyed MySQL worker.")
class connectionsPool():
"""
A MySQL workers pool
"""
def __init__(self, host, username, password, database, initialSize=16):
"""
Initialize a MySQL connections pool
:param host: MySQL host
:param username: MySQL username
:param password: MySQL password
:param database: MySQL database name
:param initialSize: initial pool size
"""
self.config = (host, username, password, database)
self.maxSize = initialSize
self.pool = queue.Queue(0)
self.consecutiveEmptyPool = 0
self.fillPool()
def newWorker(self, temporary=False):
"""
Create a new worker.
:param temporary: if True, flag the worker as temporary
:return: instance of worker class
"""
db = MySQLdb.connect(*self.config)
db.autocommit(True)
conn = worker(db, temporary)
return conn
def expandPool(self, newWorkers=5):
"""
Add some new workers to the pool
:param newWorkers: number of new workers
:return:
"""
self.maxSize += newWorkers
self.fillPool()
def fillPool(self):
"""
Fill the queue with workers until its maxSize
:return:
"""
size = self.pool.qsize()
if self.maxSize > 0 and size >= self.maxSize:
return
newConnections = self.maxSize-size
for _ in range(0, newConnections):
self.pool.put_nowait(self.newWorker())
def getWorker(self):
"""
Get a MySQL connection worker from the pool.
If the pool is empty, a new temporary worker is created.
:return: instance of worker class
"""
if self.pool.empty():
# The pool is empty. Spawn a new temporary worker
log.warning("Using temporary worker")
worker = self.newWorker(True)
# Increment saturation
self.consecutiveEmptyPool += 1
# If the pool is usually empty, expand it
if self.consecutiveEmptyPool >= 5:
log.warning("MySQL connections pool is saturated. Filling connections pool.")
self.expandPool()
else:
# The pool is not empty. Get worker from the pool
# and reset saturation counter
worker = self.pool.get()
self.consecutiveEmptyPool = 0
# Return the connection
return worker
def putWorker(self, worker):
"""
Put the worker back in the pool.
If the worker is temporary, close the connection
and destroy the object
:param worker: worker object
:return:
"""
if worker.temporary:
del worker
else:
self.pool.put_nowait(worker)
class db:
"""
A MySQL helper with multiple workers
"""
def __init__(self, host, username, password, database, initialSize):
"""
Initialize a new MySQL database helper with multiple workers.
This class is thread safe.
:param host: MySQL host
:param username: MySQL username
:param password: MySQL password
:param database: MySQL database name
:param initialSize: initial pool size
"""
self.pool = connectionsPool(host, username, password, database, initialSize)
def execute(self, query, params = ()):
"""
Executes a query
:param query: query to execute. You can bind parameters with %s
:param params: parameters list. First element replaces first %s and so on
"""
cursor = None
worker = self.pool.getWorker()
try:
# Create cursor, execute query and commit
cursor = worker.connection.cursor(MySQLdb.cursors.DictCursor)
cursor.execute(query, params)
log.debug(query)
return cursor.lastrowid
except MySQLdb.OperationalError:
del worker
worker = None
return self.execute(query, params)
finally:
# Close the cursor and release worker's lock
if cursor is not None:
cursor.close()
if worker is not None:
self.pool.putWorker(worker)
def fetch(self, query, params = (), all = False):
"""
Fetch a single value from db that matches given query
:param query: query to execute. You can bind parameters with %s
:param params: parameters list. First element replaces first %s and so on
:param all: fetch one or all values. Used internally. Use fetchAll if you want to fetch all values
"""
cursor = None
worker = self.pool.getWorker()
try:
# Create cursor, execute the query and fetch one/all result(s)
cursor = worker.connection.cursor(MySQLdb.cursors.DictCursor)
cursor.execute(query, params)
log.debug(query)
if all == True:
return cursor.fetchall()
else:
return cursor.fetchone()
except MySQLdb.OperationalError:
del worker
worker = None
return self.fetch(query, params, all)
finally:
# Close the cursor and release worker's lock
if cursor is not None:
cursor.close()
if worker is not None:
self.pool.putWorker(worker)
def fetchAll(self, query, params = ()):
"""
Fetch all values from db that matche given query.
Calls self.fetch with all = True.
:param query: query to execute. You can bind parameters with %s
:param params: parameters list. First element replaces first %s and so on
"""
return self.fetch(query, params, True)

View File

@@ -1,136 +0,0 @@
from constants import mods
from time import gmtime, strftime
import hashlib
def stringMd5(string):
"""
Return string's md5
string -- string to hash
return -- string's md5 hash
"""
d = hashlib.md5()
d.update(string.encode("utf-8"))
return d.hexdigest()
def stringToBool(s):
"""
Convert a string (True/true/1) to bool
s -- string/int value
return -- True/False
"""
return s == "True" or s == "true" or s == "1" or s == 1
def hexString(s):
"""
Output s' bytes in HEX
s -- string
return -- string with hex value
"""
return ":".join("{:02x}".format(ord(str(c))) for c in s)
def readableMods(__mods):
"""
Return a string with readable std mods.
Used to convert a mods number for oppai
__mods -- mods bitwise number
return -- readable mods string, eg HDDT
"""
r = ""
if __mods == 0:
return r
if __mods & mods.NoFail > 0:
r += "NF"
if __mods & mods.Easy > 0:
r += "EZ"
if __mods & mods.Hidden > 0:
r += "HD"
if __mods & mods.HardRock > 0:
r += "HR"
if __mods & mods.DoubleTime > 0:
r += "DT"
if __mods & mods.HalfTime > 0:
r += "HT"
if __mods & mods.Flashlight > 0:
r += "FL"
if __mods & mods.SpunOut > 0:
r += "SO"
return r
def getRank(gameMode, __mods, acc, c300, c100, c50, cmiss):
"""
Return a string with rank/grade for a given score.
Used mainly for "tillerino"
gameMode -- mode (0 = osu!, 1 = Taiko, 2 = CtB, 3 = osu!mania)
__mods -- mods bitwise number
acc -- accuracy
c300 -- 300 hit count
c100 -- 100 hit count
c50 -- 50 hit count
cmiss -- miss count
return -- rank/grade string
"""
total = c300 + c100 + c50 + cmiss
hdfl = (__mods & (mods.Hidden | mods.Flashlight | mods.FadeIn)) > 0
ss = "sshd" if hdfl else "ss"
s = "shd" if hdfl else "s"
if gameMode == 0 or gameMode == 1:
# osu!std / taiko
ratio300 = c300 / total
ratio50 = c50 / total
if ratio300 == 1:
return ss
if ratio300 > 0.9 and ratio50 <= 0.01 and cmiss == 0:
return s
if (ratio300 > 0.8 and cmiss == 0) or (ratio300 > 0.9):
return "a"
if (ratio300 > 0.7 and cmiss == 0) or (ratio300 > 0.8):
return "b"
if ratio300 > 0.6:
return "c"
return "d"
elif gameMode == 2:
# CtB
if acc == 100:
return ss
if acc > 98:
return s
if acc > 94:
return "a"
if acc > 90:
return "b"
if acc > 85:
return "c"
return "d"
elif gameMode == 3:
# osu!mania
if acc == 100:
return ss
if acc > 95:
return s
if acc > 90:
return "a"
if acc > 80:
return "b"
if acc > 70:
return "c"
return "d"
return "a"
def strContains(s, w):
return (' ' + w + ' ') in (' ' + s + ' ')
def getTimestamp():
"""
Return current time in YYYY-MM-DD HH:MM:SS format.
Used in logs.
"""
return strftime("%Y-%m-%d %H:%M:%S", gmtime())

View File

@@ -1,8 +1,9 @@
import urllib.request
import json
import urllib.request
from common.log import logUtils as log
from objects import glob
from helpers import logHelper as log
def getCountry(ip):
"""

View File

@@ -1,135 +0,0 @@
from constants import bcolors
from helpers import discordBotHelper
from helpers import generalFunctions
from objects import glob
from helpers import userHelper
import time
import os
ENDL = "\n" if os.name == "posix" else "\r\n"
def logMessage(message, alertType = "INFO", messageColor = bcolors.ENDC, discord = None, alertDev = False, of = None, stdout = True):
"""
Logs a message to stdout/discord/file
message -- message to log
alertType -- can be any string. Standard types: INFO, WARNING and ERRORS. Defalt: INFO
messageColor -- message color (see constants.bcolors). Default = bcolots.ENDC (no color)
discord -- discord channel (bunker/cm/staff/general). Optional. Default = None
alertDev -- if True, devs will receive an hl on discord. Default: False
of -- if not None but a string, log the message to that file (inside .data folder). Eg: "warnings.txt" Default: None (don't log to file)
stdout -- if True, print the message to stdout. Default: True
"""
# Get type color from alertType
if alertType == "INFO":
typeColor = bcolors.GREEN
elif alertType == "WARNING":
typeColor = bcolors.YELLOW
elif alertType == "ERROR":
typeColor = bcolors.RED
elif alertType == "CHAT":
typeColor = bcolors.BLUE
elif alertType == "DEBUG":
typeColor = bcolors.PINK
else:
typeColor = bcolors.ENDC
# Message without colors
finalMessage = "[{time}] {type} - {message}".format(time=generalFunctions.getTimestamp(), type=alertType, message=message)
# Message with colors
finalMessageConsole = "{typeColor}[{time}] {type}{endc} - {messageColor}{message}{endc}".format(
time=generalFunctions.getTimestamp(),
type=alertType,
message=message,
typeColor=typeColor,
messageColor=messageColor,
endc=bcolors.ENDC)
# Log to console
if stdout:
print(finalMessageConsole)
# Log to discord if needed
if discord is not None:
if discord == "bunker":
discordBotHelper.sendConfidential(message, alertDev)
elif discord == "cm":
discordBotHelper.sendCM(message)
elif discord == "staff":
discordBotHelper.sendStaff(message)
elif discord == "general":
discordBotHelper.sendGeneral(message)
# Log to file if needed
if of is not None:
glob.fileBuffers.write(".data/"+of, finalMessage+ENDL)
def warning(message, discord = None, alertDev = False):
"""
Log a warning to stdout (always) and discord (optional)
message -- warning message
discord -- if not None, send message to that discord channel through schiavo. Optional. Default = None
alertDev -- if True, send al hl to devs on discord. Optional. Default = False.
"""
logMessage(message, "WARNING", bcolors.YELLOW, discord, alertDev)
def error(message, discord = None, alertDev = True):
"""
Log an error to stdout (always) and discord (optional)
message -- error message
discord -- if not None, send message to that discord channel through schiavo. Optional. Default = None
alertDev -- if True, send al hl to devs on discord. Optional. Default = False.
"""
logMessage(message, "ERROR", bcolors.RED, discord, alertDev)
def info(message, discord = None, alertDev = False):
"""
Log an info message to stdout
message -- info message
discord -- if not None, send message to that discord channel through schiavo. Optional. Default = None
alertDev -- if True, send al hl to devs on discord. Optional. Default = False.
"""
logMessage(message, "INFO", bcolors.ENDC, discord, alertDev)
def debug(message):
"""
Log a debug message to stdout if server is running in debug mode
message -- debug message
"""
if glob.debug:
logMessage(message, "DEBUG", bcolors.PINK)
def chat(message):
"""
Log public messages to stdout and chatlog_public.txt
message -- chat message
"""
logMessage(message, "CHAT", bcolors.BLUE, of="chatlog_public.txt")
def pm(message):
"""
Log private messages to stdout and chatlog_private.txt
message -- chat message
"""
logMessage(message, "CHAT", bcolors.BLUE, of="chatlog_private.txt")
def rap(userID, message, discord=False, through="FokaBot"):
"""
Log a private message to Admin logs
userID -- userID of who made the action
message -- message without subject (eg: "is a meme" becomes "user is a meme")
discord -- if True, send message to discord
through -- "through" thing string. Optional. Default: "FokaBot"
"""
glob.db.execute("INSERT INTO rap_logs (id, userid, text, datetime, through) VALUES (NULL, %s, %s, %s, %s)", [userID, message, int(time.time()), through])
if discord:
username = userHelper.getUsername(userID)
logMessage("{} {}".format(username, message), discord=True)

View File

@@ -1,35 +0,0 @@
from helpers import cryptHelper
import base64
import bcrypt
def checkOldPassword(password, salt, rightPassword):
"""
Check if password+salt corresponds to rightPassword
password -- input password
salt -- password's salt
rightPassword -- right password
return -- bool
"""
return rightPassword == cryptHelper.crypt(password, "$2y$" + str(base64.b64decode(salt)))
def checkNewPassword(password, dbPassword):
"""
Check if a password (version 2) is right.
password -- input password
dbPassword -- the password in the database
return -- bool
"""
password = password.encode("utf8")
dbPassword = dbPassword.encode("utf8")
return bcrypt.hashpw(password, dbPassword) == dbPassword
def genBcrypt(password):
"""
Bcrypts a password.
password -- the password to hash.
return -- bytestring
"""
return bcrypt.hashpw(password.encode("utf8"), bcrypt.gensalt(10, b'2a'))

View File

@@ -1,101 +0,0 @@
import tornado
import tornado.web
import tornado.gen
from tornado.ioloop import IOLoop
from objects import glob
import threading
from helpers import logHelper as log
class asyncRequestHandler(tornado.web.RequestHandler):
"""
Tornado asynchronous request handler
create a class that extends this one (requestHelper.asyncRequestHandler)
use asyncGet() and asyncPost() instad of get() and post().
Done. I'm not kidding.
"""
@tornado.web.asynchronous
@tornado.gen.engine
def get(self, *args, **kwargs):
try:
yield tornado.gen.Task(runBackground, (self.asyncGet, tuple(args), dict(kwargs)))
except Exception as e:
yield tornado.gen.Task(self.captureException, exc_info=True)
finally:
if not self._finished:
self.finish()
@tornado.web.asynchronous
@tornado.gen.engine
def post(self, *args, **kwargs):
try:
yield tornado.gen.Task(runBackground, (self.asyncPost, tuple(args), dict(kwargs)))
except Exception as e:
yield tornado.gen.Task(self.captureException, exc_info=True)
finally:
if not self._finished:
self.finish()
def asyncGet(self, *args, **kwargs):
self.send_error(405)
self.finish()
def asyncPost(self, *args, **kwargs):
self.send_error(405)
self.finish()
def getRequestIP(self):
realIP = self.request.headers.get("X-Forwarded-For") if glob.cloudflare == True else self.request.headers.get("X-Real-IP")
if realIP is not None:
return realIP
return self.request.remote_ip
def runBackground(data, callback):
"""
Run a function in the background.
Used to handle multiple requests at the same time
"""
func, args, kwargs = data
def _callback(result):
#glob.busyThreads -= 1
IOLoop.instance().add_callback(lambda: callback(result))
glob.pool.apply_async(func, args, kwargs, _callback)
#threading.Thread(target=checkPoolSaturation).start()
#glob.busyThreads += 1
def checkPoolSaturation():
"""
Check the number of busy threads in connections pool.
If the pool is 100% busy, log a message to sentry
"""
size = int(glob.conf.config["server"]["threads"])
if glob.busyThreads >= size:
msg = "Connections threads pool is saturated!"
log.warning(msg)
glob.application.sentry_client.captureMessage(msg, level="warning", extra={
"workersBusy": glob.busyThreads,
"workersTotal": size
})
def checkArguments(arguments, requiredArguments):
"""
Check that every requiredArguments elements are in arguments
arguments -- full argument list, from tornado
requiredArguments -- required arguments list es: ["u", "ha"]
handler -- handler string name to print in exception. Optional
return -- True if all arguments are passed, none if not
"""
for i in requiredArguments:
if i not in arguments:
return False
return True
def printArguments(t):
"""
Print passed arguments, for debug purposes
t -- tornado object (self)
"""
print("ARGS::")
for i in t.request.arguments:
print ("{}={}".format(i, t.get_argument(i)))

View File

@@ -1,15 +1,18 @@
from objects import glob
from constants import serverPackets
from helpers import consoleHelper
import psutil
import math
import os
import signal
import sys
import threading
import signal
from helpers import logHelper as log
from constants import bcolors
import time
import math
import psutil
from common.constants import bcolors
from common.log import logUtils as log
from constants import serverPackets
from helpers import consoleHelper
from objects import glob
def dispose():
"""

View File

@@ -1,640 +0,0 @@
from helpers import passwordHelper
from constants import gameModes
from constants import privileges
from helpers import generalFunctions
from objects import glob
from helpers import logHelper as log
import time
from constants import privileges
def getID(username):
"""
Get username's user ID from userID cache (if cache hit)
or from db (and cache it for other requests) if cache miss
username -- user
return -- user id or 0
"""
# Add to cache if needed
if username not in glob.userIDCache:
userID = glob.db.fetch("SELECT id FROM users WHERE username = %s LIMIT 1", [username])
if userID == None:
return 0
glob.userIDCache[username] = userID["id"]
# Get userID from cache
return glob.userIDCache[username]
def checkLogin(userID, password):
"""
Check userID's login with specified password
db -- database connection
userID -- user id
password -- plain md5 password
return -- True or False
"""
# Get password data
passwordData = glob.db.fetch("SELECT password_md5, salt, password_version FROM users WHERE id = %s LIMIT 1", [userID])
# Make sure the query returned something
if passwordData is None:
return False
# Return valid/invalid based on the password version.
if passwordData["password_version"] == 2:
return passwordHelper.checkNewPassword(password, passwordData["password_md5"])
if passwordData["password_version"] == 1:
ok = passwordHelper.checkOldPassword(password, passwordData["salt"], passwordData["password_md5"])
if not ok: return False
newpass = passwordHelper.genBcrypt(password)
glob.db.execute("UPDATE users SET password_md5=%s, salt='', password_version='2' WHERE id = %s LIMIT 1", [newpass, userID])
def exists(userID):
"""
Check if userID exists
userID -- user ID to check
return -- bool
"""
result = glob.db.fetch("SELECT id FROM users WHERE id = %s LIMIT 1", [userID])
if result is None:
return False
else:
return True
def getSilenceEnd(userID):
"""
Get userID's **ABSOLUTE** silence end UNIX time
Remember to subtract time.time() to get the actual silence time
userID -- userID
return -- UNIX time
"""
return glob.db.fetch("SELECT silence_end FROM users WHERE id = %s LIMIT 1", [userID])["silence_end"]
def silence(userID, seconds, silenceReason, author = 999):
"""
Silence someone
userID -- userID
seconds -- silence length in seconds
silenceReason -- Silence reason shown on website
author -- userID of who silenced the user. Default: 999
"""
# db qurey
silenceEndTime = int(time.time())+seconds
glob.db.execute("UPDATE users SET silence_end = %s, silence_reason = %s WHERE id = %s LIMIT 1", [silenceEndTime, silenceReason, userID])
# Loh
targetUsername = getUsername(userID)
# TODO: exists check im drunk rn i need to sleep (stampa piede ubriaco confirmed)
if seconds > 0:
log.rap(author, "has silenced {} for {} seconds for the following reason: \"{}\"".format(targetUsername, seconds, silenceReason), True)
else:
log.rap(author, "has removed {}'s silence".format(targetUsername), True)
def getRankedScore(userID, gameMode):
"""
Get userID's ranked score relative to gameMode
userID -- userID
gameMode -- int value, see gameModes
return -- ranked score
"""
modeForDB = gameModes.getGameModeForDB(gameMode)
return glob.db.fetch("SELECT ranked_score_"+modeForDB+" FROM users_stats WHERE id = %s LIMIT 1", [userID])["ranked_score_"+modeForDB]
def getTotalScore(userID, gameMode):
"""
Get userID's total score relative to gameMode
userID -- userID
gameMode -- int value, see gameModes
return -- total score
"""
modeForDB = gameModes.getGameModeForDB(gameMode)
return glob.db.fetch("SELECT total_score_"+modeForDB+" FROM users_stats WHERE id = %s LIMIT 1", [userID])["total_score_"+modeForDB]
def getAccuracy(userID, gameMode):
"""
Get userID's average accuracy relative to gameMode
userID -- userID
gameMode -- int value, see gameModes
return -- accuracy
"""
modeForDB = gameModes.getGameModeForDB(gameMode)
return glob.db.fetch("SELECT avg_accuracy_"+modeForDB+" FROM users_stats WHERE id = %s LIMIT 1", [userID])["avg_accuracy_"+modeForDB]
def getGameRank(userID, gameMode):
"""
Get userID's **in-game rank** (eg: #1337) relative to gameMode
userID -- userID
gameMode -- int value, see gameModes
return -- game rank
"""
modeForDB = gameModes.getGameModeForDB(gameMode)
result = glob.db.fetch("SELECT position FROM leaderboard_"+modeForDB+" WHERE user = %s LIMIT 1", [userID])
if result is None:
return 0
else:
return result["position"]
def getPlaycount(userID, gameMode):
"""
Get userID's playcount relative to gameMode
userID -- userID
gameMode -- int value, see gameModes
return -- playcount
"""
modeForDB = gameModes.getGameModeForDB(gameMode)
return glob.db.fetch("SELECT playcount_"+modeForDB+" FROM users_stats WHERE id = %s LIMIT 1", [userID])["playcount_"+modeForDB]
def getUsername(userID):
"""
Get userID's username
userID -- userID
return -- username
"""
return glob.db.fetch("SELECT username FROM users WHERE id = %s LIMIT 1", [userID])["username"]
def getFriendList(userID):
"""
Get userID's friendlist
userID -- userID
return -- list with friends userIDs. [0] if no friends.
"""
# Get friends from db
friends = glob.db.fetchAll("SELECT user2 FROM users_relationships WHERE user1 = %s", [userID])
if friends is None or len(friends) == 0:
# We have no friends, return 0 list
return [0]
else:
# Get only friends
friends = [i["user2"] for i in friends]
# Return friend IDs
return friends
def addFriend(userID, friendID):
"""
Add friendID to userID's friend list
userID -- user
friendID -- new friend
"""
# Make sure we aren't adding us to our friends
if userID == friendID:
return
# check user isn't already a friend of ours
if glob.db.fetch("SELECT id FROM users_relationships WHERE user1 = %s AND user2 = %s LIMIT 1", [userID, friendID]) is not None:
return
# Set new value
glob.db.execute("INSERT INTO users_relationships (user1, user2) VALUES (%s, %s)", [userID, friendID])
def removeFriend(userID, friendID):
"""
Remove friendID from userID's friend list
userID -- user
friendID -- old friend
"""
# Delete user relationship. We don't need to check if the relationship was there, because who gives a shit,
# if they were not friends and they don't want to be anymore, be it. ¯\_(ツ)_/¯
glob.db.execute("DELETE FROM users_relationships WHERE user1 = %s AND user2 = %s", [userID, friendID])
def getCountry(userID):
"""
Get userID's country **(two letters)**.
Use countryHelper.getCountryID with what that function returns
to get osu! country ID relative to that user
userID -- user
return -- country code (two letters)
"""
return glob.db.fetch("SELECT country FROM users_stats WHERE id = %s LIMIT 1", [userID])["country"]
def getPP(userID, gameMode):
"""
Get userID's PP relative to gameMode
userID -- user
return -- PP
"""
modeForDB = gameModes.getGameModeForDB(gameMode)
return glob.db.fetch("SELECT pp_{} FROM users_stats WHERE id = %s LIMIT 1".format(modeForDB), [userID])["pp_{}".format(modeForDB)]
def setCountry(userID, country):
"""
Set userID's country (two letters)
userID -- userID
country -- country letters
"""
glob.db.execute("UPDATE users_stats SET country = %s WHERE id = %s LIMIT 1", [country, userID])
def logIP(userID, ip):
"""
User IP log
USED FOR MULTIACCOUNT DETECTION
"""
glob.db.execute("""INSERT INTO ip_user (userid, ip, occurencies) VALUES (%s, %s, 1)
ON DUPLICATE KEY UPDATE occurencies = occurencies + 1""", [userID, ip])
def saveBanchoSession(userID, ip):
"""
Save userid and ip of this token in bancho_sessions table.
Used to cache logins on LETS requests
userID --
ip -- user's ip address
"""
glob.db.execute("INSERT INTO bancho_sessions (id, userid, ip) VALUES (NULL, %s, %s)", [userID, ip])
def deleteBanchoSessions(userID, ip):
"""
Delete this bancho session from DB
userID --
ip -- user's IP address
"""
try:
glob.db.execute("DELETE FROM bancho_sessions WHERE userid = %s AND ip = %s", [userID, ip])
except:
log.warning("Token for user: {} ip: {} doesn't exist".format(userID, ip))
def is2FAEnabled(userID):
"""
Check if 2FA is enabled on an account
userID --
return -- True if 2FA is enabled, False if 2FA is disabled
"""
result = glob.db.fetch("SELECT id FROM 2fa_telegram WHERE userid = %s LIMIT 1", [userID])
return True if result is not None else False
def check2FA(userID, ip):
"""
Check if an ip is trusted
userID --
ip -- user's IP address
return -- True if the IP is untrusted, False if it's trusted
"""
if not is2FAEnabled(userID):
return False
result = glob.db.fetch("SELECT id FROM ip_user WHERE userid = %s AND ip = %s", [userID, ip])
return True if result is None else False
def getUserStats(userID, gameMode):
"""
Get all user stats relative to gameMode with only two queries
userID --
gameMode -- gameMode number
return -- dictionary with results
"""
modeForDB = gameModes.getGameModeForDB(gameMode)
# Get stats
stats = glob.db.fetch("""SELECT
ranked_score_{gm} AS rankedScore,
avg_accuracy_{gm} AS accuracy,
playcount_{gm} AS playcount,
total_score_{gm} AS totalScore,
pp_{gm} AS pp
FROM users_stats WHERE id = %s LIMIT 1""".format(gm=modeForDB), [userID])
# Get game rank
result = glob.db.fetch("SELECT position FROM leaderboard_{} WHERE user = %s LIMIT 1".format(modeForDB), [userID])
if result is None:
stats["gameRank"] = 0
else:
stats["gameRank"] = result["position"]
# Return stats + game rank
return stats
def isAllowed(userID):
"""
Check if userID is not banned or restricted
userID -- id of the user
return -- True if not banned or restricted, otherwise false.
"""
result = glob.db.fetch("SELECT privileges FROM users WHERE id = %s LIMIT 1", [userID])
if result is not None:
return (result["privileges"] & privileges.USER_NORMAL) and (result["privileges"] & privileges.USER_PUBLIC)
else:
return False
def isRestricted(userID):
"""
Check if userID is restricted
userID -- id of the user
return -- True if not restricted, otherwise false.
"""
result = glob.db.fetch("SELECT privileges FROM users WHERE id = %s LIMIT 1", [userID])
if result is not None:
return (result["privileges"] & privileges.USER_NORMAL) and not (result["privileges"] & privileges.USER_PUBLIC)
else:
return False
def isBanned(userID):
"""
Check if userID is banned
userID -- id of the user
return -- True if not banned, otherwise false.
"""
result = glob.db.fetch("SELECT privileges FROM users WHERE id = %s LIMIT 1", [userID])
if result is not None:
return not (result["privileges"] & 3 > 0)
else:
return True
def isLocked(userID):
"""
Check if userID is locked
userID -- id of the user
return -- True if not locked, otherwise false.
"""
result = glob.db.fetch("SELECT privileges FROM users WHERE id = %s LIMIT 1", [userID])
if result != None:
return ((result["privileges"] & privileges.USER_PUBLIC > 0) and (result["privileges"] & privileges.USER_NORMAL == 0))
else:
return True
def ban(userID):
"""
Ban userID
userID -- id of user
"""
banDateTime = int(time.time())
glob.db.execute("UPDATE users SET privileges = privileges & %s, ban_datetime = %s WHERE id = %s LIMIT 1", [ ~(privileges.USER_NORMAL | privileges.USER_PUBLIC | privileges.USER_PENDING_VERIFICATION) , banDateTime, userID])
def unban(userID):
"""
Unban userID
userID -- id of user
"""
glob.db.execute("UPDATE users SET privileges = privileges | %s, ban_datetime = 0 WHERE id = %s LIMIT 1", [ (privileges.USER_NORMAL | privileges.USER_PUBLIC) , userID])
def restrict(userID):
"""
Put userID in restricted mode
userID -- id of user
"""
banDateTime = int(time.time())
glob.db.execute("UPDATE users SET privileges = privileges & %s, ban_datetime = %s WHERE id = %s LIMIT 1", [~privileges.USER_PUBLIC, banDateTime, userID])
def unrestrict(userID):
"""
Remove restricted mode from userID.
Same as unban().
userID -- id of user
"""
unban(userID)
def getPrivileges(userID):
"""
Return privileges for userID
userID -- id of user
return -- privileges number
"""
result = glob.db.fetch("SELECT privileges FROM users WHERE id = %s LIMIT 1", [userID])
if result is not None:
return result["privileges"]
else:
return 0
def setPrivileges(userID, priv):
"""
Set userID's privileges in db
userID -- id of user
priv -- privileges number
"""
glob.db.execute("UPDATE users SET privileges = %s WHERE id = %s LIMIT 1", [priv, userID])
def isInPrivilegeGroup(userID, groupName):
groupPrivileges = glob.db.fetch("SELECT privileges FROM privileges_groups WHERE name = %s LIMIT 1", [groupName])
if groupPrivileges is None:
return False
groupPrivileges = groupPrivileges["privileges"]
userToken = glob.tokens.getTokenFromUserID(userID)
if userToken is not None:
userPrivileges = userToken.privileges
else:
userPrivileges = getPrivileges(userID)
return (userPrivileges == groupPrivileges) or (userPrivileges == (groupPrivileges | privileges.USER_DONOR))
def appendNotes(userID, notes, addNl = True):
"""
Append "notes" to current userID's "notes for CM"
userID -- id of user
notes -- text to append
addNl -- if True, prepend \n to notes. Optional. Default: True.
"""
if addNl:
notes = "\n"+notes
glob.db.execute("UPDATE users SET notes=CONCAT(COALESCE(notes, ''),%s) WHERE id = %s LIMIT 1", [notes, userID])
def logHardware(userID, hashes, activation = False):
"""
Hardware log
USED FOR MULTIACCOUNT DETECTION
Peppy's botnet (client data) structure (new line = "|", already split)
[0] osu! version
[1] plain mac addressed, separated by "."
[2] mac addresses hash set
[3] unique ID
[4] disk ID
return -- True if hw is not banned, otherwise false
"""
# Make sure the strings are not empty
for i in hashes[2:5]:
if i == "":
log.warning("Invalid hash set ({}) for user {} in HWID check".format(hashes, userID), "bunk")
return False
# Run some HWID checks on that user if he is not restricted
if not isRestricted(userID):
# Get username
username = getUsername(userID)
# Get the list of banned or restricted users that have logged in from this or similar HWID hash set
banned = glob.db.fetchAll("""SELECT users.id as userid, hw_user.occurencies, users.username FROM hw_user
LEFT JOIN users ON users.id = hw_user.userid
WHERE hw_user.userid != %(userid)s
AND (IF(%(mac)s!='b4ec3c4334a0249dae95c284ec5983df', hw_user.mac = %(mac)s, 1) AND hw_user.unique_id = %(uid)s AND hw_user.disk_id = %(diskid)s)
AND (users.privileges & 3 != 3)""", {
"userid": userID,
"mac": hashes[2],
"uid": hashes[3],
"diskid": hashes[4],
})
for i in banned:
# Get the total numbers of logins
total = glob.db.fetch("SELECT COUNT(*) AS count FROM hw_user WHERE userid = %s LIMIT 1", [userID])
# and make sure it is valid
if total is None:
continue
total = total["count"]
# Calculate 10% of total
perc = (total*10)/100
if i["occurencies"] >= perc:
# If the banned user has logged in more than 10% of the times from this user, restrict this user
restrict(userID)
appendNotes(userID, "-- Logged in from HWID ({hwid}) used more than 10% from user {banned} ({bannedUserID}), who is banned/restricted.".format(
hwid=hashes[2:5],
banned=i["username"],
bannedUserID=i["userid"]
))
log.warning("**{user}** ({userID}) has been restricted because he has logged in from HWID _({hwid})_ used more than 10% from banned/restricted user **{banned}** ({bannedUserID}), **possible multiaccount**.".format(
user=username,
userID=userID,
hwid=hashes[2:5],
banned=i["username"],
bannedUserID=i["userid"]
), "cm")
# Update hash set occurencies
glob.db.execute("""
INSERT INTO hw_user (id, userid, mac, unique_id, disk_id, occurencies) VALUES (NULL, %s, %s, %s, %s, 1)
ON DUPLICATE KEY UPDATE occurencies = occurencies + 1
""", [userID, hashes[2], hashes[3], hashes[4]])
# Optionally, set this hash as 'used for activation'
if activation:
glob.db.execute("UPDATE hw_user SET activated = 1 WHERE userid = %s AND mac = %s AND unique_id = %s AND disk_id = %s", [userID, hashes[2], hashes[3], hashes[4]])
# Access granted, abbiamo impiegato 3 giorni
# We grant access even in case of login from banned HWID
# because we call restrict() above so there's no need to deny the access.
return True
def resetPendingFlag(userID, success=True):
"""
Remove pending flag from an user.
userID -- ID of the user
success -- if True, set USER_PUBLIC and USER_NORMAL flags too
"""
glob.db.execute("UPDATE users SET privileges = privileges & %s WHERE id = %s LIMIT 1", [~privileges.USER_PENDING_VERIFICATION, userID])
if success:
glob.db.execute("UPDATE users SET privileges = privileges | %s WHERE id = %s LIMIT 1", [(privileges.USER_PUBLIC | privileges.USER_NORMAL), userID])
def verifyUser(userID, hashes):
# Check for valid hash set
for i in hashes[2:5]:
if i == "":
log.warning("Invalid hash set ({}) for user {} while verifying the account".format(str(hashes), userID), "bunk")
return False
# Get username
username = getUsername(userID)
# Make sure there are no other accounts activated with this exact mac/unique id/hwid
match = glob.db.fetchAll("SELECT userid FROM hw_user WHERE (IF(%(mac)s != 'b4ec3c4334a0249dae95c284ec5983df', mac = %(mac)s, 1) AND unique_id = %(uid)s AND disk_id = %(diskid)s) AND userid != %(userid)s AND activated = 1 LIMIT 1", {
"mac": hashes[2],
"uid": hashes[3],
"diskid": hashes[4],
"userid": userID
})
if match:
# This is a multiaccount, restrict other account and ban this account
# Get original userID and username (lowest ID)
originalUserID = match[0]["userid"]
originalUsername = getUsername(originalUserID)
# Ban this user and append notes
ban(userID) # this removes the USER_PENDING_VERIFICATION flag too
appendNotes(userID, "-- {}'s multiaccount ({}), found HWID match while verifying account ({})".format(originalUsername, originalUserID, hashes[2:5]))
appendNotes(originalUserID, "-- Has created multiaccount {} ({})".format(username, userID))
# Restrict the original
restrict(originalUserID)
# Discord message
log.warning("User **{originalUsername}** ({originalUserID}) has been restricted because he has created multiaccount **{username}** ({userID}). The multiaccount has been banned.".format(
originalUsername=originalUsername,
originalUserID=originalUserID,
username=username,
userID=userID
), "cm")
# Disallow login
return False
else:
# No matches found, set USER_PUBLIC and USER_NORMAL flags and reset USER_PENDING_VERIFICATION flag
resetPendingFlag(userID)
log.info("User **{}** ({}) has verified his account with hash set _{}_".format(username, userID, hashes[2:5]), "cm")
# Allow login
return True
def hasVerifiedHardware(userID):
"""
userID -- id of the user
return -- True if hwid activation data is in db, otherwise false
"""
data = glob.db.fetch("SELECT id FROM hw_user WHERE userid = %s AND activated = 1 LIMIT 1", [userID])
if data is not None:
return True
return False
def cacheUserIDs():
"""Cache userIDs in glob.userIDCache, used later with getID()."""
data = glob.db.fetchAll("SELECT id, username FROM users WHERE privileges & {} > 0".format(privileges.USER_NORMAL))
for i in data:
glob.userIDCache[i["username"]] = i["id"]
def getDonorExpire(userID):
"""
Return userID's donor expiration UNIX timestamp
:param userID:
:return: donor expiration UNIX timestamp
"""
data = glob.db.fetch("SELECT donor_expire FROM users WHERE id = %s LIMIT 1", [userID])
if data is not None:
return data["donor_expire"]
return 0