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 = {}
# 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"])
"[!] 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:
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()
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!")
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
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