Пример #1
0
from common.files import fileBuffer, fileLocks
from common.web import schiavo

try:
    with open("version") as f:
        VERSION = f.read().strip()
except:
    VERSION = "Unknown"
ACHIEVEMENTS_VERSION = 1

DATADOG_PREFIX = "lets"
BOT_NAME = "Charlotte"
db = None
redis = None
conf = None
application = None
pool = None
pascoa = {}

busyThreads = 0
debug = False
sentry = False

# Cache and objects
fLocks = fileLocks.fileLocks()
userStatsCache = userStatsCache.userStatsCache()
personalBestCache = personalBestCache.personalBestCache()
fileBuffers = fileBuffer.buffersList()
dog = datadogClient.datadogClient()
schiavo = schiavo.schiavo()
achievementClasses = {}
Пример #2
0
        # Initialize multiplayer cleanup loop
        consoleHelper.printNoNl("> Initializing multiplayer cleanup loop... ")
        glob.matches.cleanupLoop()
        consoleHelper.printDone()

        # Localize warning
        glob.localize = generalUtils.stringToBool(
            glob.conf.config["localize"]["enable"])
        if not glob.localize:
            consoleHelper.printColored(
                "[!] Warning! Users localization is disabled!", bcolors.YELLOW)

        # Discord
        if generalUtils.stringToBool(glob.conf.config["discord"]["enable"]):
            glob.schiavo = schiavo.schiavo(
                glob.conf.config["discord"]["boturl"], "**pep.py**")
        else:
            consoleHelper.printColored(
                "[!] Warning! Discord logging is disabled!", bcolors.YELLOW)

        # Gzip
        glob.gzip = generalUtils.stringToBool(
            glob.conf.config["server"]["gzip"])
        glob.gziplevel = int(glob.conf.config["server"]["gziplevel"])
        if not glob.gzip:
            consoleHelper.printColored(
                "[!] Warning! Gzip compression is disabled!", bcolors.YELLOW)

        # Debug mode
        glob.debug = generalUtils.stringToBool(
            glob.conf.config["debug"]["enable"])
Пример #3
0
                "[!] osu!api features are disabled. If you don't have a valid beatmaps table, all beatmaps will show as unranked",
                bcolors.YELLOW)
            if int(glob.conf.config["server"]["beatmapcacheexpire"]) > 0:
                consoleHelper.printColored(
                    "[!] IMPORTANT! Your beatmapcacheexpire in config.ini is > 0 and osu!api features are disabled.\nWe do not reccoment this, because too old beatmaps will be shown as unranked.\nSet beatmapcacheexpire to 0 to disable beatmap latest update check and fix that issue.",
                    bcolors.YELLOW)

        # Set achievements version
        glob.redis.set("lets:achievements_version", glob.ACHIEVEMENTS_VERSION)
        consoleHelper.printColored(
            "Achievements version is {}".format(glob.ACHIEVEMENTS_VERSION),
            bcolors.YELLOW)

        # Discord
        if generalUtils.stringToBool(glob.conf.config["discord"]["enable"]):
            glob.schiavo = schiavo.schiavo(
                glob.conf.config["discord"]["boturl"], "**lets**")
        else:
            consoleHelper.printColored(
                "[!] Warning! Discord logging is disabled!", bcolors.YELLOW)

        # Check debug mods
        glob.debug = generalUtils.stringToBool(
            glob.conf.config["server"]["debug"])
        if glob.debug:
            consoleHelper.printColored(
                "[!] Warning! Server running in debug mode!", bcolors.YELLOW)

        # Server port
        try:
            serverPort = int(glob.conf.config["server"]["port"])
        except:
Пример #4
0
		glob.redis.set("lets:achievements_version", glob.ACHIEVEMENTS_VERSION)
		consoleHelper.printColored("Achievements version is {}".format(glob.ACHIEVEMENTS_VERSION), bcolors.YELLOW)

		# Load AQL thresholds
		print("Loading AQL thresholds... ")
		try:
			glob.aqlThresholds.reload()
		except Exception as e:
			consoleHelper.printError()
			consoleHelper.printColored("[!] {}".format(e), bcolors.RED,)
			sys.exit()
		consoleHelper.printDone()

		# Discord
		if glob.conf.schiavo_enabled:
			glob.schiavo = schiavo.schiavo(glob.conf["SCHIAVO_URL"], "**lets**")
		else:
			consoleHelper.printColored("[!] Warning! Schiavo logging is disabled!", bcolors.YELLOW)

		# Server port
		try:
			if cli_args.port:
				consoleHelper.printColored("[!] Running on port {}, bypassing config.ini", bcolors.YELLOW)
				glob.serverPort = int(cli_args.port)
			else:
				glob.serverPort = glob.conf["HTTP_PORT"]
		except:
			consoleHelper.printColored("[!] Invalid server port! Please check your config.ini and run the server again", bcolors.RED)

		# Make app
		glob.application = make_app()
Пример #5
0
def main():
    parser = argparse.ArgumentParser(
        description=consoleHelper.ASCII +
        "\n\nLatest Essential Tatoe Server v{}\nBy The Ripple Team".format(
            glob.VERSION),
        formatter_class=argparse.RawTextHelpFormatter)
    parser.add_argument("-p",
                        "--port",
                        help="Run on a specific port (bypasses config.ini)",
                        required=False)
    parser.add_argument(
        "-s",
        "--stats-port",
        help="Run prometheus on a specific port (bypasses config.ini)",
        required=False)
    parser.add_argument("-q",
                        "--quiet",
                        help="Log less stuff during startup",
                        required=False,
                        default=False,
                        action="store_true")
    cli_args = parser.parse_args()

    # AGPL license agreement
    try:
        agpl.check_license("ripple", "LETS")
    except agpl.LicenseError as e:
        logging.error(str(e))
        sys.exit(1)

    try:
        if not cli_args.quiet:
            consoleHelper.printServerStartHeader(True)

        def loudLog(s, f=logging.info):
            if not cli_args.quiet:
                f(s)

        # Read config
        loudLog("Reading config file... ")
        glob.conf = Config()

        # Create data/oppai maps folder if needed
        loudLog("Checking folders... ")
        paths = (".data", glob.conf["BEATMAPS_FOLDER"],
                 glob.conf["SCREENSHOTS_FOLDER"],
                 glob.conf["FAILED_REPLAYS_FOLDER"],
                 glob.conf["REPLAYS_FOLDER"])
        for i in paths:
            if not os.path.exists(i):
                os.makedirs(i, 0o770)

        # Connect to db
        try:
            loudLog("Connecting to MySQL database")
            glob.db = dbConnector.db(host=glob.conf["DB_HOST"],
                                     port=glob.conf["DB_PORT"],
                                     user=glob.conf["DB_USERNAME"],
                                     password=glob.conf["DB_PASSWORD"],
                                     database=glob.conf["DB_NAME"],
                                     autocommit=True,
                                     charset="utf8")
            glob.db.fetch("SELECT 1")
        except:
            # Exception while connecting to db
            logging.error(
                "Error while connection to database. Please check your config.ini and run the server again"
            )
            raise

        # Connect to redis
        try:
            loudLog("Connecting to redis")
            glob.redis = redis.Redis(glob.conf["REDIS_HOST"],
                                     glob.conf["REDIS_PORT"],
                                     glob.conf["REDIS_DATABASE"],
                                     glob.conf["REDIS_PASSWORD"])
            glob.redis.ping()
        except:
            # Exception while connecting to db
            logging.error(
                "Error while connection to redis. Please check your config.ini and run the server again"
            )
            raise

        # Empty redis cache
        try:
            glob.redis.eval(
                "return redis.call('del', unpack(redis.call('keys', ARGV[1])))",
                0, "lets:*")
        except redis.exceptions.ResponseError:
            # Script returns error if there are no keys starting with peppy:*
            pass

        # Save lets version in redis
        glob.redis.set("lets:version", glob.VERSION)

        # Create threads pool
        try:
            loudLog("Creating threads pool")
            glob.pool = ThreadPool(glob.conf["THREADS"])
        except:
            logging.error(
                "Error while creating threads pool. Please check your config.ini and run the server again"
            )
            raise

        # Check osuapi
        if not glob.conf["OSU_API_ENABLE"]:
            logging.warning(
                "osu!api features are disabled. If you don't have a "
                "valid beatmaps table, all beatmaps will show as unranked")
            if glob.conf["BEATMAP_CACHE_EXPIRE"] > 0:
                logging.warning(
                    "IMPORTANT! Your beatmapcacheexpire in config.ini is > 0 and osu!api "
                    "features are disabled.\nWe do not recommend this, because too old "
                    "beatmaps will be shown as unranked.\nSet beatmapcacheexpire to 0 to "
                    "disable beatmap latest update check and fix that issue.")

        # Load achievements
        #loudLog("Loading achievements")
        #try:
        #	secret.achievements.utils.load_achievements()
        #except:
        #	logging.error("Error while loading achievements")
        #	raise

        glob.ACHIEVEMENTS_VERSION = "0.0.0"
        # Set achievements version
        glob.redis.set("lets:achievements_version", glob.ACHIEVEMENTS_VERSION)
        loudLog("Achievements version is {}".format(glob.ACHIEVEMENTS_VERSION))

        # Check if s3 is enabled
        if not glob.conf.s3_enabled:
            loudLog("S3 is disabled!", logging.warning)
        else:
            c = glob.db.fetch(
                "SELECT COUNT(*) AS c FROM s3_replay_buckets WHERE max_score_id IS NULL"
            )["c"]
            if c != 1:
                logging.error(
                    "There must be only one bucket flagged as WRITE bucket! You have {}."
                    .format(c), )
                sys.exit()

        # Discord
        if glob.conf.schiavo_enabled:
            glob.schiavo = schiavo.schiavo(glob.conf["SCHIAVO_URL"],
                                           "**lets**")
        else:
            logging.warning("Schiavo logging is disabled!")

        # Server port
        try:
            if cli_args.port:
                loudLog(
                    "Running on port {}, bypassing config.ini".format(
                        cli_args.port), logging.warning)
                glob.serverPort = int(cli_args.port)
            else:
                glob.serverPort = glob.conf["HTTP_PORT"]
        except:
            logging.error(
                "Invalid server port! Please check your config.ini and run the server again"
            )
            raise

        # Prometheus port
        try:
            if cli_args.stats_port:
                loudLog(
                    "Running stats exporter on port {}, bypassing config.ini".
                    format(cli_args.stats_port), logging.warning)
                glob.statsPort = int(cli_args.stats_port)
            elif glob.conf["PROMETHEUS_PORT"]:
                glob.statsPort = int(glob.conf["PROMETHEUS_PORT"])
        except:
            logging.error(
                "Invalid stats port! Please check your config.ini and run the server again"
            )
            raise

        # Make app
        glob.application = make_app()

        # Set up sentry
        if glob.conf.sentry_enabled:
            glob.application.sentry_client = AsyncSentryClient(
                glob.conf["SENTRY_DSN"], release=glob.VERSION)
        else:
            loudLog("Sentry logging is disabled!", logging.warning)

        # Set up Datadog
        if glob.conf.datadog_enabled:
            glob.dog = datadogClient.datadogClient(
                glob.conf["DATADOG_API_KEY"],
                glob.conf["DATADOG_APP_KEY"],
                constant_tags=["worker:{}".format(glob.serverPort)])
        else:
            glob.dog = datadogClient.datadogClient()
            loudLog("Datadog stats tracking is disabled!", logging.warning)

        # Connect to pubsub channels
        t = pubSub.listener(
            glob.redis, {
                "lets:beatmap_updates":
                beatmapUpdateHandler.handler(),
                "lets:reload_aql":
                lambda x: x == b"reload" and glob.aqlThresholds.reload(),
            })
        t.setDaemon(True)
        t.start()

        # Check debug mods
        if glob.conf["DEBUG"]:
            logging.warning("Server running in debug mode.")

        # Close main thread db connection as we don't need it anymore
        glob.threadScope.dbClose()

        # Server start message and console output
        logging.info("L.E.T.S. is listening for clients on {}:{}...".format(
            glob.conf["HTTP_HOST"], glob.serverPort))

        # log.discord("bunker", "Server started!")

        # Start Tornado
        def term(_, __):
            tornado.ioloop.IOLoop.instance().add_callback_from_signal(
                lambda: tornado.ioloop.IOLoop.instance().stop())

        signal.signal(signal.SIGINT, term)
        signal.signal(signal.SIGTERM, term)
        if glob.statsPort is not None:
            logging.info("Stats exporter listening on 0.0.0.0:{}".format(
                glob.statsPort))
            prometheus_client.start_http_server(glob.statsPort, addr="0.0.0.0")
        glob.application.listen(glob.serverPort,
                                address=glob.conf["HTTP_HOST"])
        tornado.ioloop.IOLoop.instance().start()
        logging.debug("IOLoop stopped")
    finally:
        # Perform some clean up
        logging.info("Disposing server")
        glob.fileBuffers.flushAll()
        if glob.redis.connection_pool is not None:
            glob.redis.connection_pool.disconnect()
        # TODO: properly dispose mysql connections
        if glob.pool is not None:
            # Close db conn in each thread
            glob.pool.imap(lambda *_: glob.threadScope.dbClose(),
                           [None] * glob.conf["THREADS"],
                           chunksize=1)
            # Wait for everything else to finish (should always terminate immediately)
            glob.pool.close()
            glob.pool.join()
        logging.info("Goodbye!")
Пример #6
0
def main() -> int:
    # AGPL license agreement
    try:
        agpl.check_license("ripple", "LETS")
    except agpl.LicenseError as e:
        print(str(e))
        return 1

    try:
        consoleHelper.printServerStartHeader(True)

        # Read config
        consoleHelper.printNoNl("> Reading config file... ")
        glob.conf = config.config("config.ini")

        if glob.conf.default:
            # We have generated a default config.ini, quit server
            consoleHelper.printWarning()
            consoleHelper.printColored(
                "[!] config.ini not found. A default one has been generated.",
                bcolors.YELLOW)
            consoleHelper.printColored(
                "[!] Please edit your config.ini and run the server again.",
                bcolors.YELLOW)
            return 1

        # If we haven't generated a default config.ini, check if it's valid
        if not glob.conf.checkConfig():
            consoleHelper.printError()
            consoleHelper.printColored(
                "[!] Invalid config.ini. Please configure it properly",
                bcolors.RED)
            consoleHelper.printColored(
                "[!] Delete your config.ini to generate a default one",
                bcolors.RED)
            return 1
        else:
            consoleHelper.printDone()

        # Read additional config file
        consoleHelper.printNoNl("> Loading additional config file... ")
        try:
            if not os.path.isfile(glob.conf.config["custom"]["config"]):
                consoleHelper.printWarning()
                consoleHelper.printColored(
                    "[!] Missing config file at {}; A default one has been generated at this location."
                    .format(glob.conf.config["custom"]["config"]),
                    bcolors.YELLOW)
                shutil.copy("common/default_config.json",
                            glob.conf.config["custom"]["config"])

            with open(glob.conf.config["custom"]["config"], "r") as f:
                glob.conf.extra = json.load(f)

            consoleHelper.printDone()
        except:
            consoleHelper.printWarning()
            consoleHelper.printColored(
                "[!] Unable to load custom config at {}".format(
                    glob.conf.config["custom"]["config"]), bcolors.RED)
            return 1

        # Create data/oppai maps folder if needed
        consoleHelper.printNoNl("> Checking folders... ")
        paths = [
            ".data", ".data/oppai", ".data/catch_the_pp",
            glob.conf.config["server"]["replayspath"],
            "{}_relax".format(glob.conf.config["server"]["replayspath"]),
            glob.conf.config["server"]["beatmapspath"],
            glob.conf.config["server"]["screenshotspath"]
        ]
        for i in paths:
            if not os.path.exists(i):
                os.makedirs(i, 0o770)
        consoleHelper.printDone()

        # Connect to db
        try:
            consoleHelper.printNoNl("> Connecting to MySQL database... ")
            glob.db = dbConnector.db(glob.conf.config["db"]["host"],
                                     glob.conf.config["db"]["username"],
                                     glob.conf.config["db"]["password"],
                                     glob.conf.config["db"]["database"],
                                     int(glob.conf.config["db"]["workers"]))
            consoleHelper.printNoNl(" ")
            consoleHelper.printDone()
        except:
            # Exception while connecting to db
            consoleHelper.printError()
            consoleHelper.printColored(
                "[!] Error while connection to database. Please check your config.ini and run the server again",
                bcolors.RED)
            raise

        # Connect to redis
        try:
            consoleHelper.printNoNl("> Connecting to redis... ")
            glob.redis = redis.Redis(glob.conf.config["redis"]["host"],
                                     glob.conf.config["redis"]["port"],
                                     glob.conf.config["redis"]["database"],
                                     glob.conf.config["redis"]["password"])
            glob.redis.ping()
            consoleHelper.printNoNl(" ")
            consoleHelper.printDone()
        except:
            # Exception while connecting to db
            consoleHelper.printError()
            consoleHelper.printColored(
                "[!] Error while connection to redis. Please check your config.ini and run the server again",
                bcolors.RED)
            raise

        # Empty redis cache
        #TODO: do we need this?
        try:
            glob.redis.eval(
                "return redis.call('del', unpack(redis.call('keys', ARGV[1])))",
                0, "lets:*")
        except redis.exceptions.ResponseError:
            # Script returns error if there are no keys starting with peppy:*
            pass

        # Save lets version in redis
        glob.redis.set("lets:version", glob.VERSION)

        # Create threads pool
        try:
            consoleHelper.printNoNl("> Creating threads pool... ")
            glob.pool = ThreadPool(int(glob.conf.config["server"]["threads"]))
            consoleHelper.printDone()
        except:
            consoleHelper.printError()
            consoleHelper.printColored(
                "[!] Error while creating threads pool. Please check your config.ini and run the server again",
                bcolors.RED)

        # Load achievements
        consoleHelper.printNoNl("> Loading achievements... ")
        try:
            achievements = glob.db.fetchAll("SELECT * FROM achievements")
            for achievement in achievements:
                condition = eval(
                    f"lambda score, mode_vn, stats: {achievement.pop('cond')}")
                glob.achievements.append(
                    Achievement(_id=achievement['id'],
                                file=achievement['icon'],
                                name=achievement['name'],
                                desc=achievement['description'],
                                cond=condition))
        except Exception as e:
            consoleHelper.printError()
            consoleHelper.printColored(
                "[!] Error while loading achievements! ({})".format(
                    traceback.format_exc()),
                bcolors.RED,
            )
            return 1
        consoleHelper.printDone()

        # Set achievements version
        glob.redis.set("lets:achievements_version", glob.ACHIEVEMENTS_VERSION)
        consoleHelper.printColored(
            "Achievements version is {}".format(glob.ACHIEVEMENTS_VERSION),
            bcolors.YELLOW)

        # Print disallowed mods into console (Used to also assign it into variable but has been moved elsewhere)
        unranked_mods = [
            key for key, value in glob.conf.extra["common"]
            ["rankable-mods"].items() if not value
        ]
        consoleHelper.printColored(
            "Unranked mods: {}".format(", ".join(unranked_mods)),
            bcolors.YELLOW)

        # Print allowed beatmap rank statuses
        allowed_beatmap_rank = [
            key for key, value in glob.conf.extra["lets"]
            ["allowed-beatmap-rankstatus"].items() if value
        ]
        consoleHelper.printColored(
            "Allowed beatmap rank statuses: {}".format(
                ", ".join(allowed_beatmap_rank)), bcolors.YELLOW)

        # Make array of bools to respective rank id's
        glob.conf.extra["_allowed_beatmap_rank"] = [
            getattr(rankedStatuses, key) for key in allowed_beatmap_rank
        ]  # Store the allowed beatmap rank id's into glob

        # Discord
        if generalUtils.stringToBool(glob.conf.config["discord"]["enable"]):
            glob.schiavo = schiavo.schiavo(
                glob.conf.config["discord"]["boturl"], "**lets**")
        else:
            consoleHelper.printColored(
                "[!] Warning! Discord logging is disabled!", bcolors.YELLOW)

        # Check debug mods
        glob.debug = generalUtils.stringToBool(
            glob.conf.config["server"]["debug"])
        if glob.debug:
            consoleHelper.printColored(
                "[!] Warning! Server running in debug mode!", bcolors.YELLOW)

        # Server port
        try:
            serverPort = int(glob.conf.config["server"]["port"])
        except:
            consoleHelper.printColored(
                "[!] Invalid server port! Please check your config.ini and run the server again",
                bcolors.RED)

        # Make app
        glob.application = make_app()

        # Set up sentry
        try:
            glob.sentry = generalUtils.stringToBool(
                glob.conf.config["sentry"]["enable"])
            if glob.sentry:
                glob.application.sentry_client = AsyncSentryClient(
                    glob.conf.config["sentry"]["dsn"], release=glob.VERSION)
            else:
                consoleHelper.printColored(
                    "[!] Warning! Sentry logging is disabled!", bcolors.YELLOW)
        except:
            consoleHelper.printColored(
                "[!] Error while starting Sentry client! Please check your config.ini and run the server again",
                bcolors.RED)

        # Set up Datadog
        try:
            if generalUtils.stringToBool(
                    glob.conf.config["datadog"]["enable"]):
                glob.dog = datadogClient.datadogClient(
                    glob.conf.config["datadog"]["apikey"],
                    glob.conf.config["datadog"]["appkey"])
            else:
                consoleHelper.printColored(
                    "[!] Warning! Datadog stats tracking is disabled!",
                    bcolors.YELLOW)
        except:
            consoleHelper.printColored(
                "[!] Error while starting Datadog client! Please check your config.ini and run the server again",
                bcolors.RED)

        # Connect to pubsub channels
        pubSub.listener(glob.redis, {
            "lets:beatmap_updates": beatmapUpdateHandler.handler(),
        }).start()
        # Prometheus port
        statsPort = None
        try:
            if glob.conf.config["prometheus"]["port"]:
                statsPort = int(glob.conf.config["prometheus"]["port"])
        except:
            consoleHelper.printColored(
                "Invalid stats port! Please check your config.ini and run the server again",
                bcolors.YELLOW)
            raise

        if statsPort:
            consoleHelper.printColored(
                "Stats exporter listening on localhost:{}".format(statsPort),
                bcolors.GREEN)
            prometheus_client.start_http_server(statsPort, addr="127.0.0.1")

        # Server start message and console output
        consoleHelper.printColored(
            "> L.E.T.S. is listening for clients on {}:{}...".format(
                glob.conf.config["server"]["host"], serverPort), bcolors.GREEN)

        # Start Tornado
        glob.application.listen(serverPort,
                                address=glob.conf.config["server"]["host"])
        tornado.ioloop.IOLoop.instance().start()

    finally:
        # Perform some clean up
        print("> Disposing server... ")
        glob.fileBuffers.flushAll()
        consoleHelper.printColored("Goodbye!", bcolors.GREEN)

    return 0
Пример #7
0
def handle(tornadoRequest):
    # Data to return
    responseToken = None
    responseTokenString = "ayy"
    responseData = bytes()

    # Get IP from tornado request
    requestIP = tornadoRequest.getRequestIP()

    # Avoid exceptions
    clientData = ["unknown", "unknown", "unknown", "unknown", "unknown"]
    osuVersion = "unknown"

    # Split POST body so we can get username/password/hardware data
    # 2:-3 thing is because requestData has some escape stuff that we don't need
    loginData = str(tornadoRequest.request.body)[2:-3].split("\\n")
    try:
        # Make sure loginData is valid
        if len(loginData) < 3:
            raise exceptions.invalidArgumentsException()

        # Get HWID, MAC address and more
        # 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
        # [5] Minase`s feature!
        splitData = loginData[2].split("|")
        osuVersion = splitData[0]
        timeOffset = int(splitData[1])
        clientData = splitData[3].split(":")[:5]

        if len(clientData) < 4:
            raise exceptions.forceUpdateException()

        # Try to get the ID from username
        username = str(loginData[0])
        userID = userUtils.getID(username)

        if not userID:
            # Invalid username
            raise exceptions.loginFailedException()
        if not userUtils.checkLogin(userID, loginData[1]):
            # Invalid password
            raise exceptions.loginFailedException()

        # Make sure we are not banned or locked
        priv = userUtils.getPrivileges(userID)
        if userUtils.isBanned(
                userID) and priv & privileges.USER_PENDING_VERIFICATION == 0:
            raise exceptions.loginBannedException()
        if userUtils.isLocked(
                userID) and priv & privileges.USER_PENDING_VERIFICATION == 0:
            raise exceptions.loginLockedException()

        # 2FA check
        if userUtils.check2FA(userID, requestIP):
            log.warning("Need 2FA check for user {}".format(loginData[0]))
            raise exceptions.need2FAException()

        # No login errors!

        # Verify this user (if pending activation)
        firstLogin = False
        if priv & privileges.USER_PENDING_VERIFICATION > 0 or not userUtils.hasVerifiedHardware(
                userID):
            if userUtils.verifyUser(userID, clientData):
                # Valid account
                log.info("Account {} verified successfully!".format(userID))
                glob.verifiedCache[str(userID)] = 1
                firstLogin = True
            else:
                # Multiaccount detected
                log.info("Account {} NOT verified!".format(userID))
                glob.verifiedCache[str(userID)] = 0
                raise exceptions.loginBannedException()

        # Save HWID in db for multiaccount detection
        hwAllowed = userUtils.logHardware(userID, clientData, firstLogin)

        # This is false only if HWID is empty
        # if HWID is banned, we get restricted so there's no
        # need to deny bancho access
        if not hwAllowed:
            raise exceptions.haxException()

        # Log user IP
        userUtils.logIP(userID, requestIP)

        # Delete old tokens for that user and generate a new one
        isTournament = "tourney" in osuVersion
        if not isTournament:
            glob.tokens.deleteOldTokens(userID)
        responseToken = glob.tokens.addToken(userID,
                                             requestIP,
                                             timeOffset=timeOffset,
                                             tournament=isTournament)
        responseTokenString = responseToken.token

        # Check restricted mode (and eventually send message)
        responseToken.checkRestricted()

        # Send message if donor expires soon
        if responseToken.privileges & privileges.USER_DONOR > 0:
            expireDate = userUtils.getDonorExpire(responseToken.userID)
            if expireDate - int(time.time()) <= 86400 * 3:
                expireDays = round((expireDate - int(time.time())) / 86400)
                expireIn = "{} days".format(
                    expireDays) if expireDays > 1 else "less than 24 hours"
                responseToken.enqueue(
                    serverPackets.notification(
                        "Your donor tag expires in {}! When your donor tag expires, you won't have any of the donor privileges, like yellow username, custom badge and discord custom role and username color! If you wish to keep supporting Minase and you don't want to lose your donor privileges, you can donate again by clicking on 'Support us' on Minase's website."
                        .format(expireIn)))

        # Deprecate telegram 2fa and send alert

        try:
            minaseClient = splitData[5]
            minaseClient = True
        except Exception:
            #log.logMessage(, discord="staff", of="info.txt", stdout=False)
            minaseClient = False
        # Get HWID, MAC address and more
        # 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
        # [5] Minase`s feature!
        if minaseClient == True:
            responseToken.enqueue(
                serverPackets.notification(
                    "You joined from minase!client, thank you for this :3"))
            responseToken.from_minase = True

        else:
            schiavo.schiavo(glob.conf.config['webhooks']['cm']).sendMessage(
                """{}({}) joining from default/another client
osu!version: {}

			
			""".format(loginData[0], userID, osuVersion),
                glob.conf.config['webhooks']['cm'])
        # Set silence end UNIX time in token
        responseToken.silenceEndTime = userUtils.getSilenceEnd(userID)

        # Get only silence remaining seconds
        silenceSeconds = responseToken.getSilenceSecondsLeft()

        # Get supporter/GMT
        userGMT = False
        userSupporter = True
        userTournament = False
        if responseToken.admin:
            userGMT = True
        if responseToken.privileges & privileges.USER_TOURNAMENT_STAFF > 0:
            userTournament = True

        # Server restarting check
        if glob.restarting:
            raise exceptions.banchoRestartingException()

        # Send login notification before maintenance message

        # Maintenance check
        if glob.banchoConf.config["banchoMaintenance"]:
            if not userGMT:
                # We are not mod/admin, delete token, send notification and logout
                glob.tokens.deleteToken(responseTokenString)
                raise exceptions.banchoMaintenanceException()
            else:
                # We are mod/admin, send warning notification and continue
                responseToken.enqueue(
                    serverPackets.notification(
                        "Bancho is in maintenance mode. Only mods/admins have full access to the server.\nType !system maintenance off in chat to turn off maintenance mode."
                    ))

        # Send all needed login packets
        responseToken.enqueue(serverPackets.silenceEndTime(silenceSeconds))
        responseToken.enqueue(serverPackets.userID(userID))
        responseToken.enqueue(serverPackets.protocolVersion())
        responseToken.enqueue(
            serverPackets.userSupporterGMT(userSupporter, userGMT,
                                           userTournament))
        responseToken.enqueue(serverPackets.userPanel(userID, True))
        responseToken.enqueue(serverPackets.userStats(userID, True))

        # Channel info end (before starting!?! wtf bancho?)
        responseToken.enqueue(serverPackets.channelInfoEnd())
        # Default opened channels
        # TODO: Configurable default channels
        chat.joinChannel(token=responseToken, channel="#osu")
        chat.joinChannel(token=responseToken, channel="#announce")
        glob.redis.publish('ripple:online_users',
                           int(glob.redis.get('ripple:online_users')))
        # Output channels info
        for key, value in glob.channels.channels.items():
            if value.publicRead and not value.hidden:
                responseToken.enqueue(serverPackets.channelInfo(key))

        # Send friends list
        responseToken.enqueue(serverPackets.friendList(userID))

        # Send main menu icon
        if glob.banchoConf.config["menuIcon"] != "":
            responseToken.enqueue(
                serverPackets.mainMenuIcon(glob.banchoConf.config["menuIcon"]))

        # Send online users' panels
        with glob.tokens:
            for _, token in glob.tokens.tokens.items():
                if not token.restricted:
                    responseToken.enqueue(serverPackets.userPanel(
                        token.userID))

        # Get location and country from ip.zxq.co or database
        if glob.localize:
            # Get location and country from IP
            latitude, longitude = locationHelper.getLocation(requestIP)
            countryLetters = locationHelper.getCountry(requestIP)
            country = countryHelper.getCountryID(countryLetters)
        else:
            # Set location to 0,0 and get country from db
            log.warning("Location skipped")
            latitude = 0
            longitude = 0
            countryLetters = "XX"
            country = countryHelper.getCountryID(userUtils.getCountry(userID))

        # Set location and country
        responseToken.setLocation(latitude, longitude)
        responseToken.country = country

        # Set country in db if user has no country (first bancho login)
        if userUtils.getCountry(userID) == "XX":
            userUtils.setCountry(userID, countryLetters)

        # Send to everyone our userpanel if we are not restricted or tournament
        if not responseToken.restricted:
            glob.streams.broadcast("main", serverPackets.userPanel(userID))

        # Set reponse data to right value and reset our queue
        responseData = responseToken.queue
        responseToken.resetQueue()
    except exceptions.loginFailedException:
        # Login failed error packet
        # (we don't use enqueue because we don't have a token since login has failed)
        responseData += serverPackets.loginFailed()
    except exceptions.invalidArgumentsException:
        # Invalid POST data
        # (we don't use enqueue because we don't have a token since login has failed)
        responseData += serverPackets.loginFailed()
        responseData += serverPackets.notification("???")
    except exceptions.loginBannedException:
        # Login banned error packet

        responseData += serverPackets.loginBanned()
    except exceptions.loginLockedException:
        # Login banned error packet
        responseData += serverPackets.loginLocked()
    except exceptions.banchoMaintenanceException:
        # Bancho is in maintenance mode
        responseData = bytes()
        if responseToken is not None:
            responseData = responseToken.queue
        responseData += serverPackets.rtx(
            "Our bancho server is in maintenance mode. Please try to login again later."
        )
        responseData += serverPackets.loginFailed()
    except exceptions.banchoRestartingException:
        # Bancho is restarting
        responseData += serverPackets.rtx('Bancho is restarting!')
        responseData += serverPackets.loginFailed()
    except exceptions.need2FAException:
        # User tried to log in from unknown IP
        responseData += serverPackets.needVerification()
    except exceptions.haxException:
        # Using oldoldold client, we don't have client data. Force update.
        # (we don't use enqueue because we don't have a token since login has failed)
        responseData += serverPackets.forceUpdate()
        responseData += serverPackets.notification(
            "Hory shitto, your client is TOO old! Nice prehistory! Please turn update it from the settings!"
        )
    except:
        log.error("Unknown error!\n```\n{}\n{}```".format(
            sys.exc_info(), traceback.format_exc()))
    finally:
        # Console and discord log
        if len(loginData) < 3:
            log.info(
                "Invalid bancho login request from **{}** (insufficient POST data)"
                .format(requestIP), "bunker")

        # Return token string and data
        return responseTokenString, responseData