async def logout(_, token: 'Player'): if (time.time() - token.login_time) < 5: # weird osu scheme that all already knows return await token.logout() logger.klog(f"[{token.name}] Leaved kuriso!") return True
async def silence(args: List[str], player: 'Player', _): if args: if len(args) < 2: return 'You need put amount' if len(args) < 3 or args[2].lower() not in ['s', 'm', 'h', 'd']: return 'You need to set unit s/m/h/d' if len(args) < 4: return 'You need to put reason' else: return 'I need nickname who you want to silence' target = args[0] amount = args[1] unit = args[2] reason = ' '.join(args[3:]).strip() if not amount.isdigit(): return "The amount must be a number." # Calculate silence seconds if unit == 's': silenceTime = int(amount) elif unit == 'm': silenceTime = int(amount) * 60 elif unit == 'h': silenceTime = int(amount) * 3600 elif unit == 'd': silenceTime = int(amount) * 86400 else: return "Invalid time unit (s/m/h/d)." # Max silence time is 7 days if silenceTime > 604800: return "Invalid silence time. Max silence time is 7 days." to_token = Context.players.get_token(name=target.lower()) if to_token: if to_token.id == player.id or \ to_token.privileges >= player.privileges: return 'You can\'t silence that dude' await to_token.silence(silenceTime, reason, player.id) logger.klog(f"[Player/{to_token.name}] has been silenced for following reason: {reason}") return 'User silenced' offline_user = await userHelper.get_start_user(target.lower()) if offline_user['privileges'] > player.privileges: return 'You can\'t silence that dude' if not offline_user: return 'User not found!' res = await userHelper.silence(offline_user['id'], silenceTime, reason, player.id) if not res: return 'Not silenced!' logger.klog(f"[Player/{offline_user['username']}] has been silenced for following reason: {reason}") return 'User successfully silenced'
async def activate_user(user_id: int, user_name: str, hashes: Union[Tuple[str], List[str]]) -> bool: if len(hashes) < 5 or not all((x for x in hashes)): logger.elog( f"[Verification/{user_id}] have wrong hash set! Probably generated by randomizer" ) return False match: dict if hashes[2] == "b4ec3c4334a0249dae95c284ec5983df" or \ hashes[4] == "ffae06fb022871fe9beb58b005c5e21d": # user logins from wine(old bancho checks) match = await Context.mysql.fetch( "select userid from hw_user where unique_id = %(unique_id)s and userid != %(userid)s and activated = 1 limit 1", { 'unique_id': hashes[3], 'userid': user_id }) else: # its 100%(prob 80%) windows match = await Context.mysql.fetch( 'select userid from hw_user ' 'where mac = %(mac)s and unique_id = %(unique_id)s ' 'and disk_id = %(disk_id)s ' 'and userid != %(userid)s ' 'and activated = 1 LIMIT 1', { "mac": hashes[2], "unique_id": hashes[3], "disk_id": hashes[4], "userid": user_id }) await Context.mysql.execute( 'update users set privileges = privileges & %s where id = %s limit 1', [~Privileges.USER_PENDING_VERIFICATION, user_id]) if match: source_user_id = match['userid'] source_user_name = (await get_username(source_user_id)) # баним его await ban(source_user_id) # уведомляем стафф, что это читерюга и как-бы ну нафиг. await append_notes(user_id, [ f"{source_user_name}\'s multiaccount ({hashes[2:5]}),found HWID match while verifying account ({user_id})" ]) await append_notes( source_user_id, [f"Has created multiaccount {user_name} ({user_id})"]) logger.klog( f"[{source_user_name}] Has created multiaccount {user_name} ({user_id})" ) return False await Context.mysql.execute( "UPDATE users SET privileges = privileges | %s WHERE id = %s LIMIT 1", [(Privileges.USER_PUBLIC | Privileges.USER_NORMAL), user_id]) return True
async def send_message(self, message: 'Message') -> bool: message.body = f'{message.body[:2045]}...' if message.body[ 2048:] else message.body chan: str = message.to if chan.startswith("#"): channel: 'Channel' = Context.channels.get(chan, None) if not channel: logger.klog( f"[{self.name}/Bot] Tried to send message in unknown channel. Ignoring it..." ) return False logger.klog( f"{self.name}({self.id})/Bot -> {channel.server_name}: {bytes(message.body, 'latin_1').decode()}" ) await channel.send_message(self.id, message) return True # DM receiver = Context.players.get_token( name=message.to.lower().strip().replace(" ", "_")) if not receiver: logger.klog(f"[{self.name}] Tried to offline user. Ignoring it...") return False logger.klog( f"#DM {self.name}({self.id})/Bot -> {message.to}({receiver.id}): {bytes(message.body, 'latin_1').decode()}" ) receiver.enqueue(await PacketBuilder.BuildMessage(self.id, message)) return True
async def send_private_message(packet_data: bytes, token: 'Player'): if token.silenced: logger.klog( f"[{token.name}] This bruh tried to send message, when he is muted" ) return False message = await PacketResolver.read_message(packet_data) message.client_id = token.id await token.send_message( Message(sender=token.name, body=message.body, to=message.to, client_id=token.id)) await CrystalBot.proceed_command(message) return True
async def remove_silence(args: List[str], player: 'Player', _): if not args: return 'I need nickname who you want to remove the silence' target = args[0] to_token = Context.players.get_token(name=target.lower()) if to_token: logger.klog(f"[Player/{to_token.name}] silence reset") await to_token.silence(0, "", player.id) return 'User silenced' offline_user = await userHelper.get_start_user(target.lower()) if not offline_user: return 'User not found!' res = await userHelper.silence(offline_user['id'], 0, "", player.id) if not res: return 'Not silenced!' logger.klog(f"[Player/{offline_user['username']}] silence reset") return 'User successfully silenced'
async def leave_channel(self, p: 'Player') -> bool: if p not in self.users: return False # enqueue leave channel p.enqueue(await PacketBuilder.PartChannel(self.name)) self.users.pop(self.users.index(p)) logger.klog(f"[{p.name}] Parted from {self.server_name}") # now we need update channel stats if self.temp_channel: receivers = self.users else: receivers = Context.players.get_all_tokens() for receiver in receivers: receiver.enqueue(await PacketBuilder.ChannelAvailable(self)) if len(self.users) < 1 and self.temp_channel: # clean channel because all left and channel is temp(for multi lobby or spectator) Context.channels.pop(self.server_name) return True
async def join_channel(self, p: 'Player') -> bool: if p in self.users: p.enqueue(await PacketBuilder.SuccessJoinChannel(self.name)) return True if not self.can_read and not self.is_privileged(p.privileges): logger.klog( f"[{p.name}] Tried to join private channel {self.server_name} but haven't enough staff " "permissions") return False # enqueue join channel p.enqueue(await PacketBuilder.SuccessJoinChannel(self.name)) self.users.append(p) logger.klog(f"[{p.name}] Joined to {self.server_name}") # now we need update channel stats if self.temp_channel: receivers = self.users else: receivers = Context.players.get_all_tokens() for receiver in receivers: receiver.enqueue(await PacketBuilder.ChannelAvailable(self)) return True
async def send_message(self, message: 'Message') -> bool: message.body = f'{message.body[:2045]}...' if message.body[ 2048:] else message.body chan: str = message.to if chan.startswith("#"): # this is channel object if chan.startswith("#multi"): if self.is_tourneymode: if self.id_tourney > 0: chan = f"#multi_{self.id_tourney}" else: return False else: chan = f"#multi_{self.match.id}" elif chan.startswith("#spec"): if self.spectating: chan = f"#spec_{self.spectating.id}" else: chan = f"#spec_{self.id}" channel: 'Channel' = Context.channels.get(chan, None) if not channel: logger.klog( f"[{self.name}] Tried to send message in unknown channel. Ignoring it..." ) return False self.user_chat_log.append(message) logger.klog( f"{self.name}({self.id}) -> {channel.server_name}: {bytes(message.body, 'latin_1').decode()}" ) await channel.send_message(self.id, message) return True # DM receiver = Context.players.get_token( name=message.to.lower().strip().replace(" ", "_")) if not receiver: logger.klog(f"[{self.name}] Tried to offline user. Ignoring it...") return False if receiver.pm_private and self.id not in receiver.friends: self.enqueue(await PacketBuilder.PMBlocked(message.to)) logger.klog( f"[{self.name}] Tried message {message.to} which has private PM." ) return False if self.pm_private and receiver.id not in self.friends: self.pm_private = False logger.klog( f"[{self.name}] which has private pm sended message to non-friend user. PM unlocked" ) if receiver.silenced: self.enqueue(await PacketBuilder.TargetSilenced(message.to)) logger.klog( f'[{self.name}] Tried message {message.to}, but has been silenced.' ) return False self.user_chat_log.append(message) logger.klog( f"#DM {self.name}({self.id}) -> {message.to}({receiver.id}): {bytes(message.body, 'latin_1').decode()}" ) receiver.enqueue(await PacketBuilder.BuildMessage(self.id, message)) return True
async def main_handler(request: Request): if request.headers.get("user-agent", "") != "osu!": return HTMLResponse(f"<html>{Context.motd_html}</html>") token = request.headers.get("osu-token", None) if token: if token == '': response = await PacketBuilder.UserID(-5) return BanchoResponse(response) # send to re-login token_object = Context.players.get_token(token=token) if not token_object: # send to re-login, because token doesn't exists in storage response = await PacketBuilder.UserID(-5) return BanchoResponse(response) token_object.last_packet_unix = int(time.time()) # packets recieve raw_bytes = KurisoBuffer(None) await raw_bytes.write_to_buffer(await request.body()) response = bytearray() while not raw_bytes.EOF(): packet_id = await raw_bytes.read_u_int_16() _ = await raw_bytes.read_int_8() # empty byte packet_length = await raw_bytes.read_int_32() if packet_id == OsuPacketID.Client_Pong.value: # client just spamming it and tries to say, that he is normal :sip: continue data = await raw_bytes.slice_buffer(packet_length) if token_object.is_restricted and packet_id not in ALLOWED_RESTRICT_PACKETS: logger.wlog( f"[{token_object.token}/{token_object.name}] Ignored packet {packet_id}(account restrict)" ) continue if packet_id in OsuEvent.handlers: # This packet can be handled by OsuEvent Class, call it now! # Oh wait let go this thing in async executor. await OsuEvent.handlers[packet_id](data, token_object) logger.klog( f"[{token_object.token}/{token_object.name}] Has triggered {OsuPacketID(packet_id)} with packet length: {packet_length}" ) else: logger.wlog( f"[Events] Packet ID: {packet_id} not found in events handlers" ) response += token_object.dequeue() response = BanchoResponse(bytes(response), token=token_object.token) return response else: # first login # 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 start_time = time.time() # auth speed benchmark time loginData = (await request.body()).decode().split("\n") if len(loginData) < 3: return BanchoResponse(await PacketBuilder.UserID(-5)) if not await userHelper.check_login(loginData[0], loginData[1], request.client.host): logger.elog( f"[{loginData[0]}] tried to login but failed with password") return BanchoResponse(await PacketBuilder.UserID(-1)) user_data = await userHelper.get_start_user(loginData[0]) if not user_data: return BanchoResponse(await PacketBuilder.UserID(-1)) data = loginData[2].split("|") hashes = data[3].split(":")[:-1] time_offset = int(data[1]) pm_private = data[4] == '1' isTourney = "tourney" in data[0] # check if user already on kuriso if Context.players.get_token(uid=user_data['id']) and not isTourney: # wtf osu await Context.players.get_token(uid=user_data['id']).logout() if (user_data['privileges'] & Privileges.USER_PENDING_VERIFICATION) or \ not await userHelper.user_have_hardware(user_data['id']): # we need to verify our user is_success_verify = await userHelper.activate_user( user_data['id'], user_data['username'], hashes) if not is_success_verify: response = (await PacketBuilder.UserID( -1 ) + await PacketBuilder.Notification( 'Your HWID is not clear. Contact Staff to create account!') ) return BanchoResponse(bytes(response)) else: user_data['privileges'] = KurikkuPrivileges.Normal.value await Context.mysql.execute( "UPDATE hw_user SET activated = 1 WHERE userid = %s AND mac = %s AND unique_id = %s AND disk_id = %s", [user_data['id'], hashes[2], hashes[3], hashes[4]]) if (user_data["privileges"] & KurikkuPrivileges.Normal) != KurikkuPrivileges.Normal and \ (user_data["privileges"] & Privileges.USER_PENDING_VERIFICATION) == 0: logger.elog(f"[{loginData}] Banned chmo tried to login") response = (await PacketBuilder.UserID( -1 ) + await PacketBuilder.Notification( 'You are banned. Join our discord for additional information.') ) return BanchoResponse(bytes(response)) if ((user_data["privileges"] & Privileges.USER_PUBLIC > 0) and (user_data["privileges"] & Privileges.USER_NORMAL == 0)) \ and (user_data["privileges"] & Privileges.USER_PENDING_VERIFICATION) == 0: logger.elog(f"[{loginData}] Locked dude tried to login") response = ( await PacketBuilder.UserID(-1) + await PacketBuilder.Notification( 'You are locked by staff. Join discord and ask for unlock!' )) return BanchoResponse(bytes(response)) if bool(Context.bancho_settings['bancho_maintenance']): # send to user that maintenance if not (user_data['privileges'] & KurikkuPrivileges.Developer): response = (await PacketBuilder.UserID( -1 ) + await PacketBuilder.Notification( 'Kuriso! is in maintenance mode. Please try to login again later.' )) return BanchoResponse(bytes(response)) await Context.mysql.execute( ''' INSERT INTO hw_user (userid, mac, unique_id, disk_id, occurencies) VALUES (%s, %s, %s, %s, 1) ON DUPLICATE KEY UPDATE occurencies = occurencies + 1''', [user_data['id'], hashes[2], hashes[3], hashes[4]] ) # log hardware и не ебёт что osu_version = data[0] await userHelper.setUserLastOsuVer(user_data['id'], osu_version) osuVersionInt = osu_version[1:9] now = datetime.datetime.now() vernow = datetime.datetime(int(osuVersionInt[:4]), int(osuVersionInt[4:6]), int(osuVersionInt[6:8]), 00, 00) deltanow = now - vernow if not osuVersionInt[0].isdigit() or \ deltanow.days > 360 or int(osuVersionInt) < 20200811: response = (await PacketBuilder.UserID( -2 ) + await PacketBuilder.Notification( 'Sorry, you use outdated/bad osu!version. Please update your game to join server' )) return BanchoResponse(bytes(response)) if isTourney: if Context.players.get_token(uid=user_data['id']): # manager was logged before, we need just add additional token token, player = Context.players.get_token( uid=user_data['id']).add_additional_client() else: player = TourneyPlayer( int(user_data['id']), user_data['username'], user_data['privileges'], time_offset, pm_private, 0 if user_data['silence_end'] - int(time.time()) < 0 else user_data['silence_end'] - int(time.time()), is_tourneymode=True, ip=request.client.host) await asyncio.gather(*[ player.parse_friends(), player.update_stats(), player.parse_country(request.client.host) ]) else: # create Player instance finally!!!! player = Player( int(user_data['id']), user_data['username'], user_data['privileges'], time_offset, pm_private, 0 if user_data['silence_end'] - int(time.time()) < 0 else user_data['silence_end'] - int(time.time()), ip=request.client.host) await asyncio.gather(*[ player.parse_friends(), player.update_stats(), player.parse_country(request.client.host) ]) if "ppy.sh" in request.url.netloc and not ( player.is_admin or (player.privileges & KurikkuPrivileges.TournamentStaff == KurikkuPrivileges.TournamentStaff)): return BanchoResponse( bytes(await PacketBuilder.UserID( -5 ) + await PacketBuilder.Notification( 'Sorry, you use outdated connection to server. Please use devserver flag' ))) user_country = await userHelper.get_country(user_data['id']) if user_country == "XX": await userHelper.set_country(user_data['id'], player.country[1]) start_bytes_async = await asyncio.gather(*[ PacketBuilder.UserID(player.id), PacketBuilder.ProtocolVersion(19), PacketBuilder.BanchoPrivileges(player.bancho_privs), PacketBuilder.UserPresence(player), PacketBuilder.UserStats(player), PacketBuilder.FriendList(player.friends), PacketBuilder.SilenceEnd( player.silence_end if player.silence_end > 0 else 0), PacketBuilder.Notification( f'''Welcome to kuriso!\nBuild ver: v{Context.version}\nCommit: {Context.commit_id}''' ), PacketBuilder.Notification( f'Authorization took: {round((time.time() - start_time) * 1000, 4)}ms' ) ]) start_bytes = b''.join(start_bytes_async) if Context.bancho_settings.get('login_notification', None): start_bytes += await PacketBuilder.Notification( Context.bancho_settings.get('login_notification', None)) if Context.bancho_settings.get('bancho_maintenance', None): start_bytes += await PacketBuilder.Notification( 'Don\'t forget enable server after maintenance :sip:') if Context.bancho_settings['menu_icon']: start_bytes += await PacketBuilder.MainMenuIcon( Context.bancho_settings['menu_icon']) if isTourney and Context.players.get_token(uid=user_data['id']): logger.klog( f"[{player.token}/{player.name}] Joined kuriso as additional client for origin!" ) for p in Context.players.get_all_tokens(): if p.is_restricted: continue start_bytes += bytes(await PacketBuilder.UserPresence(p) + await PacketBuilder.UserStats(p)) else: for p in Context.players.get_all_tokens(): if p.is_restricted: continue start_bytes += bytes(await PacketBuilder.UserPresence(p) + await PacketBuilder.UserStats(p)) p.enqueue( bytes(await PacketBuilder.UserPresence(player) + await PacketBuilder.UserStats(player))) await userHelper.saveBanchoSession(player.id, request.client.host) Context.players.add_token(player) await Context.redis.set("ripple:online_users", len(Context.players.get_all_tokens(True))) logger.klog(f"[{player.token}/{player.name}] Joined kuriso!") # default channels to join is #osu, #announce and #english await asyncio.gather(*[ Context.channels['#osu'].join_channel( player), Context.channels['#announce'].join_channel(player), Context.channels['#english'].join_channel(player) ]) for (_, chan) in Context.channels.items(): if not chan.temp_channel and chan.can_read: start_bytes += await PacketBuilder.ChannelAvailable(chan) start_bytes += await PacketBuilder.ChannelListeningEnd() if player.is_restricted: start_bytes += await PacketBuilder.UserRestricted() await CrystalBot.ez_message( player.name, "Your account is currently in restricted mode. Please visit kurikku's website for more information." ) Context.stats['osu_versions'].labels(osu_version=osu_version).inc() Context.stats['devclient_usage'].labels(host=request.url.netloc).inc() return BanchoResponse(start_bytes, player.token)
async def sub_reader(ch: aioredis.Channel): while await ch.wait_message(): if ch.name in MAPPED_FUNCTIONS: logger.klog(f"[Redis/Pubsub] Received event in {ch.name}") await MAPPED_FUNCTIONS[ch.name](ch)
async def main(): # load dotenv file load_dotenv(find_dotenv()) # Load configuration for our project Config.load_config() logger.slog("[Config] Loaded") # create simple Starlette through uvicorn app app = Starlette(debug=Config.config['debug']) app.add_middleware(ProxyHeadersMiddleware) if Config.config['sentry']['enabled']: sentry_sdk.init(dsn=Config.config['sentry']['url']) app.add_middleware(SentryMiddleware) # load version Context.load_version() logger.klog(f"Hey! Starting kuriso! v{Context.version} (commit-id: {Context.commit_id})") logger.printColored(open("kuriso.MOTD", mode="r", encoding="utf-8").read(), logger.YELLOW) # Load all events & handlers registrator.load_handlers(app) # Create Redis connection :sip: logger.wlog("[Redis] Trying connection to Redis") redis_values = dict( db=Config.config['redis']['db'], minsize=5, maxsize=10 ) if Config.config['redis']['password']: redis_values['password'] = Config.config['redis']['password'] redis_pool = await aioredis.create_redis_pool( f"redis://{Config.config['redis']['host']}", **redis_values ) Context.redis = redis_pool logger.slog("[Redis] Connection to Redis established! Well done!") logger.slog("[Redis] Removing old information about redis...") try: await Context.redis.set("ripple:online_users", "0") redis_flush_script = ''' local matches = redis.call('KEYS', ARGV[1]) local result = 0 for _,key in ipairs(matches) do result = result + redis.call('DEL', key) end return result ''' await Context.redis.eval(redis_flush_script, args=["peppy:*"]) await Context.redis.eval(redis_flush_script, args=["peppy:sessions:*"]) except Exception as e: traceback.print_exc() capture_exception(e) logger.elog("[Redis] initiation data ruined... Check this!") await Context.redis.set("peppy:version", Context.version) logger.wlog("[MySQL] Making connection to MySQL Database...") mysql_pool = AsyncSQLPoolWrapper() await mysql_pool.connect(**{ 'host': Config.config['mysql']['host'], 'user': Config.config['mysql']['user'], 'password': Config.config['mysql']['password'], 'port': Config.config['mysql']['port'], 'db': Config.config['mysql']['database'], 'loop': asyncio.get_event_loop(), 'autocommit': True }) Context.mysql = mysql_pool logger.slog("[MySQL] Connection established!") if Config.config['prometheus']['enabled']: logger.wlog("[Prometheus stats] Loading...") prometheus_client.start_http_server( Config.config['prometheus']['port'], addr=Config.config['prometheus']['host'] ) logger.slog("[Prometheus stats] Metrics started...") # now load bancho settings await Context.load_bancho_settings() await registrator.load_default_channels() from bot.bot import CrystalBot # now load bot await CrystalBot.connect() # and register bot commands CrystalBot.load_commands() logging.getLogger('apscheduler.executors.default').setLevel(logging.WARNING) scheduler = AsyncIOScheduler() scheduler.start() scheduler.add_job(loops.clean_timeouts, "interval", seconds=60) if Config.config['prometheus']['enabled']: scheduler.add_job(loops.add_prometheus_stats, "interval", seconds=15) scheduler.add_job(loops.add_stats, "interval", seconds=120) # Setup pub/sub listeners for LETS/old admin panel events event_loop = asyncio.get_event_loop() event_loop.create_task(pubsub_listeners.init()) Context.load_motd() uvicorn.run(app, host=Config.config['host']['address'], port=Config.config['host']['port'], access_log=False)