Exemplo n.º 1
0
    async def parse_country(self, ip: str) -> bool:
        if self.privileges & Privileges.USER_DONOR:
            # we need to remember donor have locked location
            donor_location: str = (await Context.mysql.fetch(
                'select country from users_stats where id = %s',
                [self.id]))['country'].upper()
            self.country = (Countries.get_country_id(donor_location),
                            donor_location)
        else:
            data = None
            async with aiohttp.ClientSession() as sess:
                async with sess.get(Config.config['geoloc_ip'] + ip) as resp:
                    try:
                        data = await resp.json()
                    finally:
                        pass

            if not data:
                logger.elog(f"[Player/{self.name}] Can't parse geoloc")
                return False

            self.country = (Countries.get_country_id(data['country']),
                            data['country'])
            loc = data['loc'].split(",")
            self.location = (float(loc[0]), float(loc[1]))
            return True
Exemplo n.º 2
0
    async def add_spectator(self, new_spec: 'Player') -> bool:
        spec_chan_name = f"#spec_{self.id}"
        if not Context.channels.get(spec_chan_name):
            # in this case, we need to create channel for our spectator in temp mode
            spec = Channel(server_name=spec_chan_name,
                           description=f"Spectator channel for {self.name}",
                           public_read=True,
                           public_write=True,
                           temp_channel=True)

            Context.channels[spec_chan_name] = spec
            await spec.join_channel(self)

        c: 'Channel' = Context.channels.get(spec_chan_name)
        if not await c.join_channel(new_spec):
            logger.elog(
                f"{self.name} failed to join in {spec_chan_name} spectator channel!"
            )
            return False

        fellow_packet = await PacketBuilder.FellowSpectatorJoined(new_spec.id)
        for spectator in self.spectators:
            spectator.enqueue(fellow_packet)
            new_spec.enqueue(await
                             PacketBuilder.FellowSpectatorJoined(spectator.id))

        self.spectators.append(new_spec)
        new_spec.spectating = self

        self.enqueue(await PacketBuilder.SpectatorJoined(new_spec.id))
        logger.slog(f"{new_spec.name} started to spectating {self.name}!")
        return True
Exemplo n.º 3
0
async def presence_update(packet_data: bytes, p: 'Player'):
    data = await PacketResolver.read_pr_filter(packet_data)

    if not 0 <= data < 3:
        logger.elog(f"[Player/{p.name}] Tried to set bad pr filter")
        return

    p.presence_filter = PresenceFilter(data)
Exemplo n.º 4
0
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
Exemplo n.º 5
0
async def channel_join(packet_data: bytes, token: 'Player'):
    chan_name = await PacketResolver.read_channel_name(packet_data)
    if not chan_name.startswith("#"):
        return

    chan: 'Channel' = Context.channels.get(chan_name, None)
    if not chan:
        logger.elog(f'[{token.name}] Failed to join in {chan_name}')
        return False

    await chan.join_channel(token)
    return True
Exemplo n.º 6
0
async def leave_spectator(_, token: 'Player'):
    old_victim = token.spectating

    if not old_victim:
        logger.elog(
            f"{token.name} tried to stop spectating empty old victim...")
        return False  # because this is bug and impossible in context

    if token.is_tourneymode:
        await old_victim.remove_hidden_spectator(token)
    else:
        await old_victim.remove_spectator(token)
    return True
Exemplo n.º 7
0
async def cant_spectate(_, token: 'Player'):
    if not token.spectating:
        logger.elog(
            f"{token.name} sent that he can't spectate, but he is not spectating..."
        )
        return False  # impossible condition

    packet = await PacketBuilder.CantSpectate(token.id)
    token.spectating.enqueue(packet)  # send this sweet packet lol

    for recv in token.spectating.spectators:
        recv.enqueue(packet)

    return True
Exemplo n.º 8
0
    async def update_stats(self, selected_mode: GameModes = None) -> bool:
        for mode in GameModes if not selected_mode else [selected_mode]:
            res = await Context.mysql.fetch(
                'select total_score_{0} as total_score, ranked_score_{0} as ranked_score, '
                'pp_{0} as pp, playcount_{0} as total_plays, avg_accuracy_{0} as accuracy, playtime_{0} as playtime '
                'from users_stats where id = %s'.format(
                    GameModes.resolve_to_str(mode)), [self.id])

            if not res:
                logger.elog(
                    f"[Player/{self.name}] Can't parse stats for {GameModes.resolve_to_str(mode)}"
                )
                return False

            res['leaderboard_rank'] = 0

            self.stats[mode].update(**res)
        return True
Exemplo n.º 9
0
async def join_spectator(packet_data: bytes, token: 'Player'):
    to_spectate_id = await PacketResolver.read_specatator_id(packet_data)

    player_spec = Context.players.get_token(uid=to_spectate_id)
    if not player_spec:
        logger.elog(
            f"{token.name} failed to spectate non-exist user with id {to_spectate_id}"
        )
        return False

    if player_spec:
        token.enqueue(await PacketBuilder.UserStats(player_spec) +
                      await PacketBuilder.UserPresence(player_spec))

    if token.spectating:
        # remove old spectating, because we found new victim
        await token.spectating.remove_spectator(token)

    await player_spec.add_spectator(token)
    return True
Exemplo n.º 10
0
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)
Exemplo n.º 11
0
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)
Exemplo n.º 12
0
    async def _shutdown():
        logger.elog("[System] Disposing server!")
        logger.elog("[System] Disposing players!")
        for player in Context.players.get_all_tokens():
            await player.say_bancho_restarting()

        logger.elog("[Server] Awaiting when players will get them packets!")
        attempts = 0
        while any(len(x.queue) > 0 for x in Context.players.get_all_tokens()):
            await asyncio.sleep(5)
            attempts += 1
            logger.elog(f"[Server] Attempt {attempts}/3")
            if attempts == 3:
                break

        # Stop redis connection
        logger.elog("[Server] Stopping redis pool...")
        if Context.redis:
            Context.redis.close()
            await Context.redis.wait_closed()

        # Stop redis sub connection
        logger.elog("[Server] Stopping redis subscriber pool...")
        if Context.redis_sub:
            Context.redis_sub.close()
            await Context.redis_sub.wait_closed()

        # Stop mysql pool connection
        logger.elog("[Server] Stopping mysql pool...")
        if Context.mysql:
            Context.mysql.pool.close()
            await Context.mysql.pool.wait_closed()

        logger.elog("[Server] Disposing uvicorn instance...")
Exemplo n.º 13
0
    async def proceed_command(cls, message: 'Message') -> Union[bool]:
        if message.sender == cls.bot_name:
            return False

        sender = Context.players.get_token(uid=message.client_id)
        if not sender:
            return False

        if message.to.startswith("#multi"):
            # convert it into normal
            if sender.match:
                message.to = f"#multi_{sender.match.id}"
        if message.to.startswith("#spec"):
            if sender.spectating:
                message.to = f"#spec_{sender.spectating.id}"

        message.body = message.body.strip()
        cmd, func_command = None, None
        for (k, func) in cls.commands.items():
            if message.body.startswith(k):
                cmd, func_command = k, func
                break

        if not cmd:
            return False

        comand = cmd
        args = shlex.split(message.body[len(cmd):].replace("'", "\\'").replace(
            '"', '\\"'),
                           posix=True)

        cdUser = cls.cd.get(sender.id, None)
        nowTime = int(time.time())
        if cdUser:
            if nowTime - cdUser <= cls.cool_down:  # Checking users cooldown
                cls.cd[sender.id] = nowTime
                return False

            cls.cd[sender.id] = nowTime
        else:  # If user not write something after bot running
            cls.cd[sender.id] = nowTime

        result = None
        try:
            result = await func_command(args, sender, message)
        except Exception as e:
            logger.elog(f"[Bot] {sender.name} with {comand} crashed {args}")
            capture_exception(e)
            traceback.print_exc()
            return await cls.token.send_message(
                Message(sender=cls.token.name,
                        body='Command crashed, write to KotRik!!!',
                        to=message.sender,
                        client_id=cls.token.id))

        if result:
            await cls.token.send_message(
                Message(sender=cls.token.name,
                        body=result,
                        to=message.to
                        if message.to.startswith("#") else message.sender,
                        client_id=cls.token.id))
        return True