Esempio n. 1
0
class TestVkApiClient(unittest.TestCase):
    @classmethod
    def setUpClass(cls):
        cls.mock_server_port = get_free_port()
        start_mock_server(cls.mock_server_port)

    def test_request_response(self):
        mock_users_url = 'http://localhost:{port}/'.format(
            port=self.mock_server_port)
        with mock.patch('сlasses.vk_api_client.VkApiClient.API_BASE_URL',
                        new_callable=mock.PropertyMock) as mock_f:
            mock_f.return_value = mock_users_url
            self.api = VkApiClient(token='',
                                   app_id='',
                                   user_id='1',
                                   debug_mode=True)
            assert self.api.is_initialized
            assert self.api.get_fname == 'Павел'
            assert self.api.get_lname == 'Дуров'
            assert len(self.api.get_countries()) == 234
            assert len(self.api.search_cities(country_id=1,
                                              city_name='Нижн')) == 80
            assert len(self.api.search_users(q='Дуров')) == 16
            assert len(self.api.get_user_photos(owner_id='1',
                                                needed_qty=1000)) == 9
    def test_users_save_load(self):
        mock_users_url = 'http://localhost:{port}/'.format(
            port=self.mock_server_port)
        with mock.patch('сlasses.vk_api_client.VkApiClient.API_BASE_URL',
                        new_callable=mock.PropertyMock) as mock_f:
            mock_f.return_value = mock_users_url
            self.api = VkApiClient(token='',
                                   app_id='',
                                   user_id='1',
                                   debug_mode=True)
            assert self.api.is_initialized
            users = self.api.get_users(['1', '5', 1])
            client_1 = VKinderClient(users[0])
            assert client_1
            client_2 = VKinderClient(users[1])
            assert client_2

            self.db.save_client(client_1)
            self.db.save_client(client_2)

            for i in range(self.db.search_history_limit + 1):
                client_1.reset_search()
                client_2.reset_search()

                client_1.search.sex_id = randrange(0, 2, 1)
                client_1.search.status_id = randrange(1, 8, 1)
                client_1.search.city_id = 1
                client_1.search.city_name = 'Москва'
                client_1.search.min_age = randrange(0, 60, 1)
                client_1.search.max_age = randrange(client_1.search.min_age,
                                                    127, 1)
                client_1.rating_filter = 0

                client_2.search.sex_id = randrange(0, 2, 1)
                client_2.search.status_id = randrange(1, 8, 1)
                client_2.search.city_id = 2
                client_2.search.city_name = 'Санкт-Петербург'
                client_2.search.min_age = randrange(0, 60, 1)
                client_2.search.max_age = randrange(client_2.search.min_age,
                                                    127, 1)
                client_2.rating_filter = 0

                self.db.save_search(client_1)
                self.db.save_search(client_2)

                client_1.found_users = self.api.search_users()
                client_2.found_users = self.api.search_users(q='babych')
                assert len(client_1.found_users) > 0
                assert len(client_2.found_users) > 0

                self.db.save_users(client_1)
                self.db.save_users(client_2)
Esempio n. 3
0
 def __init__(self,
              group_token: str,
              person_token: str,
              group_id: str,
              app_id: str,
              db_name: str,
              db_login: str,
              db_password: str,
              db_driver: str,
              db_host: str,
              db_port: int,
              retry_timeout: int = 1,
              retry_attempts: int = sys.maxsize,
              debug_mode=False):
     self.client_activity_timeout = 300
     self.debug_mode = debug_mode
     self.clients_pool = {}
     self.group_id = group_id
     self.cmd = Commands(COMMANDS)
     # countries received once per application launch (by request), as they almost doesn't changes
     self.countries = []
     self.rebuild_tables = False
     self.retry_timeout = retry_timeout
     self.retry_attempts = retry_attempts
     self.vk_personal = VkApiClient(person_token,
                                    app_id,
                                    debug_mode=debug_mode)
     self.db = VKinderDb(db_name,
                         db_login,
                         db_password,
                         db_driver=db_driver,
                         db_host=db_host,
                         db_port=db_port,
                         debug_mode=debug_mode)
     self.__initialized = self.vk_personal.is_initialized and self.db.is_initialized
     self.vk_group = vk_api.VkApi(token=group_token)
     try:
         self.long_poll = VkBotLongPoll(self.vk_group, self.group_id)
         self.vk_api = self.vk_group.get_api()
     except BaseException as e:
         self.__initialized = False
         log(f'{type(self).__name__} init failed: {e.error["error_msg"]}',
             self.debug_mode)
     if self.__initialized:
         log(f'{type(self).__name__} initialised successfully',
             self.debug_mode)
 def test_client_save_load(self):
     assert self.db.is_initialized
     mock_users_url = 'http://localhost:{port}/'.format(
         port=self.mock_server_port)
     with mock.patch('сlasses.vk_api_client.VkApiClient.API_BASE_URL',
                     new_callable=mock.PropertyMock) as mock_f:
         mock_f.return_value = mock_users_url
         self.api = VkApiClient(token='',
                                app_id='',
                                user_id='1',
                                debug_mode=True)
         assert self.api.is_initialized
         users = self.api.get_users('1')
         assert len(users) == 2
         user = users[0]
         client = VKinderClient(user)
         self.db.save_client(client)
         client_db = self.db.load_client_from_db('1')
         assert client_db
         client = VKinderClient(client_db)
         assert client.vk_id == '1'
         assert client.fname == 'Павел'
         assert client.lname == 'Дуров'
Esempio n. 5
0
class VKinderBot:
    def __init__(self,
                 group_token: str,
                 person_token: str,
                 group_id: str,
                 app_id: str,
                 db_name: str,
                 db_login: str,
                 db_password: str,
                 db_driver: str,
                 db_host: str,
                 db_port: int,
                 retry_timeout: int = 1,
                 retry_attempts: int = sys.maxsize,
                 debug_mode=False):
        self.client_activity_timeout = 300
        self.debug_mode = debug_mode
        self.clients_pool = {}
        self.group_id = group_id
        self.cmd = Commands(COMMANDS)
        # countries received once per application launch (by request), as they almost doesn't changes
        self.countries = []
        self.rebuild_tables = False
        self.retry_timeout = retry_timeout
        self.retry_attempts = retry_attempts
        self.vk_personal = VkApiClient(person_token,
                                       app_id,
                                       debug_mode=debug_mode)
        self.db = VKinderDb(db_name,
                            db_login,
                            db_password,
                            db_driver=db_driver,
                            db_host=db_host,
                            db_port=db_port,
                            debug_mode=debug_mode)
        self.__initialized = self.vk_personal.is_initialized and self.db.is_initialized
        self.vk_group = vk_api.VkApi(token=group_token)
        try:
            self.long_poll = VkBotLongPoll(self.vk_group, self.group_id)
            self.vk_api = self.vk_group.get_api()
        except BaseException as e:
            self.__initialized = False
            log(f'{type(self).__name__} init failed: {e.error["error_msg"]}',
                self.debug_mode)
        if self.__initialized:
            log(f'{type(self).__name__} initialised successfully',
                self.debug_mode)

    def send_typing_activity(self, client: VKinderClient):
        """
        Imitation of keyboard activity from bot, suitable if we'll make network requests
        """
        self.vk_api.messages.setActivity(type='typing', peer_id=client.vk_id)

    def send_msg(self,
                 client: VKinderClient,
                 message: str,
                 attachment: str = None,
                 keyboard=None):
        """
        Sends message with smart break using VK max message size, splits message by specified dividers
        """
        # message += f'\n[step={get_dict_key_by_value(STATUSES, client.status)}, ' \
        #            f'фильтр: {get_dict_key_by_value(RATINGS, client.rating_filter)}]' if self.debug_mode else ''
        for msg in break_str(message, max_size=MAX_MSG_SIZE):
            self.vk_api.messages.send(peer_id=client.vk_id,
                                      message=msg,
                                      attachment=attachment,
                                      random_id=randrange(10**7),
                                      keyboard=keyboard)

    def get_client(self, vk_id) -> VKinderClient:
        """
        Here we create new instance of clients class and add to clients pool
        If client already exists, we check his activity timeout and reset client if timeout expired
        """
        client = self.clients_pool.get(vk_id, None)
        if client:
            lag = int((datetime.now() - client.last_contact).total_seconds())
            if lag > self.client_activity_timeout:
                self.do_send_to_start_after_absence(client)
        else:
            user = self.vk_personal.get_users(vk_id)[0]
            client = VKinderClient(user)
            self.db.save_client(client)
            self.clients_pool.update({user.vk_id: client})
            self.do_greet_client(client)
        return client

    def start(self):
        retries = 0
        if not self.__initialized:
            log(f'Can\'t start: {type(self).__name__} not initialized',
                self.debug_mode)
            return
        while True and retries < self.retry_attempts:
            try:
                retries += 1
                log(
                    f'Listening for messages in group {self.group_id}...(retry #{retries})',
                    self.debug_mode)
                self.long_poll.update_longpoll_server()
                for event in self.long_poll.listen():
                    if event.type == VkBotEventType.MESSAGE_NEW:
                        client = self.get_client(
                            str(event.object.message['from_id']))
                        msg = event.object.message['text']
                        log(f'[{client.fname} {client.lname}] typed "{msg}"',
                            self.debug_mode)
                        msg = msg.lower()

                        # obligatory exit from conversation, works at any page, highest priority,
                        # one exception: if this command not very first one
                        if msg in self.cmd.get(
                                'quit'
                        ) and client.status != STATUSES['has_contacted']:
                            self.do_say_goodbye(client)
                            continue

                        # imitation of custom search - works at any page, highest priority
                        if msg == 'test':
                            client.reset_search()
                            client.search.sex_id = randrange(0, 2, 1)
                            client.search.status_id = randrange(1, 8, 1)
                            client.search.city_id = 1
                            client.search.city_name = 'Москва'
                            client.search.min_age = randrange(0, 60, 1)
                            client.search.max_age = randrange(
                                client.search.min_age, 127, 1)
                            client.rating_filter = RATINGS['new']
                            self.do_users_search(client)
                            continue

                        # if client prints/press something at "Welcome screen" page
                        if client.status in (STATUSES['invited'], STATUSES['has_contacted']) \
                                and (msg in self.cmd.get('yes') or msg in self.cmd.get('new search')):
                            self.do_start_search_creating(client)

                        elif client.status in (STATUSES['invited'], STATUSES['has_contacted']) \
                                and msg in self.cmd.get('show history'):
                            self.do_show_search_history(client)

                        elif client.status in (STATUSES['invited'], STATUSES['has_contacted']) \
                                and (msg in self.cmd.get('liked') or msg in self.cmd.get('disliked') or msg in
                                     self.cmd.get('banned')):
                            self.do_show_rated_users(msg, client)

                        elif client.status in (STATUSES['invited'],
                                               STATUSES['has_contacted']
                                               ) and msg in self.cmd.get('no'):
                            self.do_say_goodbye(client)

                        elif client.status == STATUSES['has_contacted']:
                            self.do_propose_start_search(client)

                        # if client prints/press something in "Select search history" page
                        elif client.status == STATUSES[
                                'search_history_input_wait'] and msg in self.cmd.get(
                                    'back'):
                            self.do_propose_start_search(client)

                        # if client prints/press something in "Select search history" page
                        elif client.status == STATUSES[
                                'search_history_input_wait']:
                            self.on_search_history_choose(msg, client)

                        # if client prints/press something in "Search country" page
                        elif client.status == STATUSES[
                                'country_input_wait'] and msg in self.cmd.get(
                                    'back'):
                            self.do_start_search_creating(client)

                        # if client prints/press something in "Search country" page
                        elif client.status == STATUSES['country_input_wait']:
                            self.on_country_name_input(msg, client)

                        # if client prints/press something in "Select country" page
                        elif client.status == STATUSES[
                                'country_choose_wait'] and msg in self.cmd.get(
                                    'back'):
                            self.do_propose_country_name_input(client)

                        # if client prints/press something in "Select country" page
                        elif client.status == STATUSES['country_choose_wait']:
                            self.on_country_name_choose(msg, client)

                        # if client prints/press something in "Search city" page
                        elif client.status == STATUSES[
                                'city_input_wait'] and msg in self.cmd.get(
                                    'back'):
                            self.do_propose_start_search(client)

                        # if client prints/press something in "Search city" page
                        elif client.status == STATUSES[
                                'city_input_wait'] and msg in self.cmd.get(
                                    'country'):
                            self.do_propose_country_name_input(client)

                        # if client prints/press something in "Search city" page
                        elif client.status == STATUSES['city_input_wait']:
                            self.do_propose_city_name_choose(msg, client)

                        # if client prints/press something in "Select city" page
                        elif client.status == STATUSES[
                                'city_choose_wait'] and msg in self.cmd.get(
                                    'back'):
                            self.do_start_search_creating(client)

                        # if client prints/press something in "Select city" page
                        elif client.status == STATUSES['city_choose_wait']:
                            self.on_city_name_choose(msg, client)

                        # if client prints/press something in "Select sex" page
                        elif client.status == STATUSES[
                                'sex_choose_wait'] and msg in self.cmd.get(
                                    'back'):
                            self.do_start_search_creating(client)

                        # if client prints/press something in "Select sex" page
                        elif client.status == STATUSES['sex_choose_wait']:
                            self.on_sex_choose(msg, client)

                        # if client prints/press something in "Select love status" page
                        elif client.status == STATUSES[
                                'status_choose_wait'] and msg in self.cmd.get(
                                    'back'):
                            self.do_propose_sex_choose(client)

                        # if client prints/press something in "Select love status" page
                        elif client.status == STATUSES['status_choose_wait']:
                            self.on_status_choose(msg, client)

                        # if client prints/press something in "Min age" page
                        elif client.status == STATUSES[
                                'min_age_input_wait'] and msg in self.cmd.get(
                                    'back'):
                            self.do_propose_status_choose(client)

                        # if client prints/press something in "Min age" page
                        elif client.status == STATUSES['min_age_input_wait']:
                            self.on_min_age_enter(msg, client)

                        # if client prints/press something in "Max age" page
                        elif client.status == STATUSES[
                                'max_age_input_wait'] and msg in self.cmd.get(
                                    'back'):
                            self.do_propose_min_age_enter(client)

                        # if client prints/press something in "Max age" page
                        elif client.status == STATUSES['max_age_input_wait']:
                            self.on_max_age_enter(msg, client)

                        # if client prints/press something in "User profile view" page
                        elif client.status == STATUSES[
                                'decision_wait'] and msg in self.cmd.get(
                                    'back'):
                            if client.rating_filter == RATINGS['new']:
                                self.do_propose_min_age_enter(client)
                            else:
                                self.do_propose_start_search(client)

                        # if client prints/press something in "User profile view" page
                        elif client.status == STATUSES['decision_wait']:
                            self.on_decision_made(msg, client)

                        # if command is unexpected or not recognized
                        else:
                            self.do_inform_about_unknown_command(client)
            except (requests.exceptions.ConnectionError,
                    requests.exceptions.ReadTimeout,
                    urllib3.exceptions.ReadTimeoutError):
                if retries < self.retry_attempts:
                    log(
                        f'Error in connection. Retry in {self.retry_timeout} seconds...',
                        self.debug_mode)
                    sleep(self.retry_timeout)
                else:
                    log(f'Error in connection. Bot shutting down.',
                        self.debug_mode)

    # @decorator_speed_meter(True)
    def on_decision_made(self, msg: str, client: VKinderClient):
        if msg in self.cmd.get('yes'):
            client.active_user.rating_id = RATINGS['liked']
        elif msg in self.cmd.get('no'):
            client.active_user.rating_id = RATINGS['disliked']
        elif msg in self.cmd.get('ban'):
            client.active_user.rating_id = RATINGS['banned']
        else:
            self.do_inform_about_unknown_command(client)
            return
        self.db.save_user_rating(client)
        self.do_show_next_user(client)

    # @decorator_speed_meter(True)
    def do_show_next_user(self, client: VKinderClient):
        self.send_typing_activity(client)
        client.status = STATUSES['decision_wait']
        client.active_user = client.get_next_user()
        # if we already showed all pairs
        if not client.active_user:
            self.do_send_to_start_due_to_reach_end(client)
            self.do_propose_start_search(client)
            return
        client.active_user.photos = self.vk_personal.get_user_photos(
            client.active_user.vk_id)
        if not client.active_user.photos:
            client.active_user.photos = self.vk_personal.get_user_photos(
                client.active_user.vk_id, album_id='wall')
        photos = [
            f'photo{photo.owner_id}_{photo.id}'
            for photo in client.active_user.photos
        ]
        photos_str = ','.join(photos)
        log(
            f'[{client.fname} {client.lname}] Showing user: {client.active_user.fname} '
            f'{client.active_user.lname} with {len(client.active_user.photos)} photos',
            self.debug_mode)
        age_str = f', возраст: {client.active_user.age}' if client.active_user.age else ''
        user_info = f'{client.active_user.fname} {client.active_user.lname} '
        user_info += f'({client.active_user.city_name}{age_str})'
        user_info += f'{last_seen(client.active_user.last_seen_time)}\nhttps://vk.com/{client.active_user.domain}'
        keyboard = self.cmd.kb(['yes', 'no', 'ban', None, 'back', 'quit'])
        self.send_msg(client,
                      user_info,
                      attachment=photos_str,
                      keyboard=keyboard)
        self.send_msg(client, PHRASES['do_you_like_it'])
        self.db.save_photos(client)

    def do_show_rated_users(self, msg: str, client: VKinderClient):
        client.status = STATUSES['loading_users']
        if msg in self.cmd.get('banned'):
            client.rating_filter = RATINGS['banned']
        elif msg in self.cmd.get('disliked'):
            client.rating_filter = RATINGS['disliked']
        else:
            client.rating_filter = RATINGS['liked']
        self.db.load_users_from_db(client)
        if client.found_users:
            self.do_show_next_user(client)
        else:
            self.send_msg(client, PHRASES['no_peoples_found'])
            self.do_propose_start_search(client)

    # @decorator_speed_meter(True)
    def do_users_search(self, client: VKinderClient):
        client.status = STATUSES['loading_users']
        params = PHRASES['city_x_sex_x_status_x_age_xx'].format(
            client.search.city_name, SEXES[client.search.sex_id],
            LOVE_STATUSES[client.search.status_id], client.search.min_age,
            client.search.max_age)
        self.send_msg(client,
                      f'{PHRASES["started_search_peoples"]}\n({params})')
        self.send_typing_activity(client)
        self.db.save_search(client)
        client.found_users = self.vk_personal.search_users(
            city_id=client.search.city_id,
            sex_id=client.search.sex_id,
            love_status_id=client.search.status_id,
            age_from=client.search.min_age,
            age_to=client.search.max_age)
        client.found_users = [
            user for user in client.found_users
            if not user.is_closed and user.last_seen_time
        ]
        client.found_users.sort(key=lambda x: x.last_seen_time, reverse=True)
        if client.found_users:
            self.db.load_users_ratings_from_db(client)
            ratings_sum = get_users_ratings_counts(client.found_users)
            self.send_msg(
                client,
                PHRASES['found_x_peoples_x_new_x_liked_x_disliked_x_banned'].
                format(len(client.found_users), ratings_sum['new'],
                       ratings_sum['liked'], ratings_sum['disliked'],
                       ratings_sum['banned']))
            if ratings_sum['new'] > 0:
                self.db.save_users(client)
                self.do_show_next_user(client)
            else:
                self.send_msg(client, PHRASES['no_new_peoples_found'])
                self.do_propose_start_search(client)
        else:
            self.send_msg(client, PHRASES['no_peoples_found'])
            self.do_propose_start_search(client)

    # @decorator_speed_meter(True)
    def on_max_age_enter(self, max_age: str, client: VKinderClient):
        result = None
        try:
            max_age = int(max_age) + 1
            if 0 < max_age <= 128:
                result = max_age
        except ValueError:
            pass
        if result:
            if client.search.min_age > result - 1:
                self.send_msg(client, PHRASES['minimal_age_more_maximal_age'])
                self.do_propose_min_age_enter(client)
            else:
                client.search.max_age = result - 1
                self.send_msg(
                    client,
                    PHRASES['you_chosen_min_age_x_and_max_age_x'].format(
                        client.search.min_age, client.search.max_age))
                self.do_users_search(client)
        else:
            self.send_msg(client, PHRASES['error_in_age'])
            self.do_propose_min_age_enter(client)

    # @decorator_speed_meter(True)
    def do_propose_max_age_enter(self, client: VKinderClient):
        client.status = STATUSES['max_age_input_wait']
        keyboard = self.cmd.kb(['back', 'quit'])
        self.send_msg(client,
                      PHRASES['enter_max_age_from_x_127'].format(
                          client.search.min_age),
                      keyboard=keyboard)

    # @decorator_speed_meter(True)
    def on_min_age_enter(self, min_age: str, client: VKinderClient):
        result = None
        try:
            min_age = int(min_age) + 1
            if 0 < min_age <= 128:
                result = min_age
        except ValueError:
            pass
        if result:
            client.search.min_age = result - 1
            self.do_propose_max_age_enter(client)
        else:
            self.send_msg(client, PHRASES['error_in_age'])
            self.do_propose_min_age_enter(client)

    # @decorator_speed_meter(True)
    def do_propose_min_age_enter(self, client: VKinderClient):
        client.status = STATUSES['min_age_input_wait']
        keyboard = self.cmd.kb(['back', 'quit'])
        self.send_msg(client,
                      PHRASES['enter_min_age_from_0_127'],
                      keyboard=keyboard)

    # @decorator_speed_meter(True)
    def on_status_choose(self, status_id: str, client: VKinderClient):
        result = None
        try:
            status_id = int(status_id)
            if 0 < status_id <= len(LOVE_STATUSES):
                result = status_id
        except ValueError:
            pass
        if result:
            client.search.status_id = result
            self.send_msg(
                client, PHRASES['you_chosen_love_status_x'].format(
                    LOVE_STATUSES[result]))
            self.do_propose_min_age_enter(client)
        else:
            self.send_msg(client, PHRASES['no_such_love_status_in_list'])
            self.do_propose_status_choose(client)

    # @decorator_speed_meter(True)
    def do_propose_status_choose(self, client: VKinderClient):
        client.status = STATUSES['status_choose_wait']
        statuses = [
            f'{status_id}. {status}'
            for status_id, status in LOVE_STATUSES.items()
        ]
        keyboard = self.cmd.kb(['back', 'quit'])
        self.send_msg(client, '\n'.join(statuses), keyboard=keyboard)
        self.send_msg(client, PHRASES['choose_love_status_number'])

    # @decorator_speed_meter(True)
    def on_sex_choose(self, sex: str, client: VKinderClient):
        result = None
        try:
            if sex in self.cmd.get('woman'):
                result = get_dict_key_by_value(SEXES, 'женщина') + 1
            elif sex in self.cmd.get('man'):
                result = get_dict_key_by_value(SEXES, 'мужчина') + 1
            elif sex in self.cmd.get('anybody'):
                result = get_dict_key_by_value(SEXES, 'любой') + 1
            else:
                sex = int(sex) + 1
                if 0 < sex <= len(SEXES):
                    result = sex
        except ValueError:
            pass
        if result:
            client.search.sex_id = result - 1
            self.send_msg(
                client, PHRASES['you_chosen_sex_x'].format(
                    SEXES[client.search.sex_id]))
            self.do_propose_status_choose(client)
        else:
            self.send_msg(client, PHRASES['no_such_sex_in_list'])
            self.do_propose_sex_choose(client)

    # @decorator_speed_meter(True)
    def do_propose_sex_choose(self, client: VKinderClient):
        client.status = STATUSES['sex_choose_wait']
        sexes = [f'{sex_id}. {sex}' for sex_id, sex in SEXES.items()]
        keyboard = self.cmd.kb(
            ['woman', 'man', 'anybody', None, 'back', 'quit'])
        self.send_msg(client, '\n'.join(sexes), keyboard=keyboard)
        self.send_msg(client, PHRASES['choose_sex_number'])

    # @decorator_speed_meter(True)
    def on_city_name_choose(self, city: str, client: VKinderClient):
        result = None
        try:
            city = int(city)
            if 0 < city <= len(client.found_cities):
                result = city
        except ValueError:
            pass
        if result:
            client.search.city_id = client.found_cities[result - 1].id
            client.search.city_name = client.found_cities[result - 1].title
            self.send_msg(
                client,
                PHRASES['you_chosen_city_x'].format(client.search.city_name))
            self.do_propose_sex_choose(client)
        else:
            self.send_msg(client, PHRASES['no_such_city_in_list'])
            self.do_propose_city_name_choose('', client)

    # @decorator_speed_meter(True)
    def do_propose_city_name_choose(self, city: str, client: VKinderClient):
        client.status = STATUSES['city_choose_wait']
        self.send_typing_activity(client)
        # this needed to prevent repeated search operations with same city name
        if city:
            client.found_cities = self.vk_personal.search_cities(
                country_id=client.country_id, city_name=city)
        cities = [
            f'{index}. {format_city_name(city)}'
            for index, city in enumerate(client.found_cities, 1)
        ]
        if cities:
            keyboard = self.cmd.kb(['back', 'quit'])
            self.send_msg(client, '\n'.join(cities), keyboard=keyboard)
            self.send_msg(client, PHRASES['choose_city_number'])
        else:
            self.send_msg(client, PHRASES['no_such_city_name'])
            self.do_start_search_creating(client)

    # @decorator_speed_meter(True)
    def on_country_name_choose(self, country_id: str, client: VKinderClient):
        result = None
        try:
            country_id = int(country_id)
            if 0 < country_id <= len(client.found_countries):
                result = country_id
        except ValueError:
            pass
        if result:
            client.country_id = client.found_countries[result - 1].id
            client.country_name = client.found_countries[result - 1].title
            self.db.save_client(client, force_country_update=True)
            self.send_msg(
                client,
                PHRASES['you_chosen_country_x'].format(client.country_name))
            self.do_start_search_creating(client)
        else:
            self.send_msg(client, PHRASES['no_such_country_in_list'])

    # @decorator_speed_meter(True)
    def on_country_name_input(self, country_name: str, client: VKinderClient):
        client.status = STATUSES['country_choose_wait']
        self.send_typing_activity(client)
        # this needed to prevent repeated search operations for countries names
        if not self.countries:
            self.countries = self.vk_personal.get_countries()
        # filter countries by country_name
        client.found_countries = [
            country for country in self.countries
            if country.title.lower().find(country_name) > -1
        ]
        countries = [
            f'{index}. {country.title}'
            for index, country in enumerate(client.found_countries, 1)
        ]
        if countries:
            keyboard = self.cmd.kb(['back', 'quit'])
            self.send_msg(client, '\n'.join(countries), keyboard=keyboard)
            self.send_msg(client, PHRASES['choose_country_number'])
        else:
            self.send_msg(client, PHRASES['no_such_city_name'])
            self.do_propose_country_name_input(client)

    # @decorator_speed_meter(True)
    def do_propose_country_name_input(self, client: VKinderClient):
        client.status = STATUSES['country_input_wait']
        keyboard = self.cmd.kb(['back', 'quit'])
        self.send_msg(client, PHRASES['enter_country_name'], keyboard=keyboard)

    # @decorator_speed_meter(True)
    def do_start_search_creating(self, client: VKinderClient):
        client.status = STATUSES['city_input_wait']
        # revert to default for new search
        client.rating_filter = RATINGS['new']
        client.reset_search()
        keyboard = self.cmd.kb(['country', None, 'back', 'quit'])
        self.send_msg(client,
                      PHRASES['enter_city_name_in_x'].format(
                          client.country_name),
                      keyboard=keyboard)

    # @decorator_speed_meter(True)
    def on_search_history_choose(self, history_id: str, client: VKinderClient):
        result = None
        try:
            history_id = int(history_id)
            if 0 < history_id <= len(client.searches):
                result = history_id
        except ValueError:
            pass
        if result:
            client.search = client.searches[result - 1]
            self.do_users_search(client)
        else:
            self.send_msg(client, PHRASES['no_such_history_in_list'])
            self.do_show_search_history(client)

    # @decorator_speed_meter(True)
    def do_show_search_history(self, client: VKinderClient):
        client.status = STATUSES['search_history_input_wait']
        client.rating_filter = RATINGS['new']
        searches = self.db.load_searches(client)
        client.searches = searches
        history = []
        for index, searches in enumerate(searches, 1):
            history.append(
                str(index) + '. ' + PHRASES['x_x_x_from_x_to_x'].format(
                    searches.city_name, SEXES[searches.sex_id], LOVE_STATUSES[
                        searches.status_id], searches.min_age,
                    searches.max_age))
        if history:
            keyboard = self.cmd.kb(['back', 'quit'])
            self.send_msg(client, '\n'.join(history), keyboard=keyboard)
            self.send_msg(client, PHRASES['choose_search_history_number'])
        else:
            self.send_msg(client, PHRASES['no_search_history'])
            self.do_propose_start_search(client)

    # @decorator_speed_meter(True)
    def do_propose_start_search(self, client: VKinderClient):
        client.status = STATUSES['invited']
        client.rating_filter = RATINGS['new']
        if len(client.searches) == 0:
            keyboard = self.cmd.kb(['yes', 'no'])
            self.send_msg(client,
                          PHRASES['do_you_want_to_find_pair'],
                          keyboard=keyboard)
        else:
            keyboard = self.cmd.kb([
                'new search', 'show history', None, 'liked', 'disliked',
                'banned', None, 'quit'
            ])
            self.send_msg(client,
                          PHRASES['you_have_search_history'],
                          keyboard=keyboard)

    # @decorator_speed_meter(True)
    def do_greet_client(self, client: VKinderClient):
        self.send_msg(client, PHRASES['greetings_x'].format(client.fname))

    # @decorator_speed_meter(True)
    def do_send_to_start_after_absence(self, client: VKinderClient):
        client.status = STATUSES['has_contacted']
        self.send_msg(
            client, PHRASES['sorry_x_you_was_absent_for_x_seconds'].format(
                client.fname, self.client_activity_timeout))

    # @decorator_speed_meter(True)
    def do_send_to_start_due_to_reach_end(self, client: VKinderClient):
        client.status = STATUSES['has_contacted']
        self.send_msg(client, PHRASES['well_lets_start_again'])

    # @decorator_speed_meter(True)
    def do_inform_about_unknown_command(self, client: VKinderClient):
        client.last_contact = datetime.now()
        self.send_msg(client, PHRASES['sorry_i_dont_understand_you'])

    # @decorator_speed_meter(True)
    def do_say_goodbye(self, client: VKinderClient):
        if len(client.searches) == 0:
            keyboard = self.cmd.kb(['yes', 'no'])
            self.send_msg(client,
                          PHRASES['goodbye_x'].format(client.fname),
                          keyboard=keyboard)
        else:
            keyboard = self.cmd.kb([
                'new search', 'show history', None, 'liked', 'disliked',
                'banned', None, 'quit'
            ])
            self.send_msg(client,
                          PHRASES['goodbye_x'].format(client.fname),
                          keyboard=keyboard)
        self.clients_pool.pop(client.vk_id)
class TestVKinderDb(unittest.TestCase):
    @classmethod
    def setUpClass(cls):
        cls.db = VKinderDb('test', 'test', 'test', debug_mode=True)
        cls.mock_server_port = get_free_port()
        start_mock_server(cls.mock_server_port)

    def test_client_save_load(self):
        assert self.db.is_initialized
        mock_users_url = 'http://localhost:{port}/'.format(
            port=self.mock_server_port)
        with mock.patch('сlasses.vk_api_client.VkApiClient.API_BASE_URL',
                        new_callable=mock.PropertyMock) as mock_f:
            mock_f.return_value = mock_users_url
            self.api = VkApiClient(token='',
                                   app_id='',
                                   user_id='1',
                                   debug_mode=True)
            assert self.api.is_initialized
            users = self.api.get_users('1')
            assert len(users) == 2
            user = users[0]
            client = VKinderClient(user)
            self.db.save_client(client)
            client_db = self.db.load_client_from_db('1')
            assert client_db
            client = VKinderClient(client_db)
            assert client.vk_id == '1'
            assert client.fname == 'Павел'
            assert client.lname == 'Дуров'

    def test_users_save_load(self):
        mock_users_url = 'http://localhost:{port}/'.format(
            port=self.mock_server_port)
        with mock.patch('сlasses.vk_api_client.VkApiClient.API_BASE_URL',
                        new_callable=mock.PropertyMock) as mock_f:
            mock_f.return_value = mock_users_url
            self.api = VkApiClient(token='',
                                   app_id='',
                                   user_id='1',
                                   debug_mode=True)
            assert self.api.is_initialized
            users = self.api.get_users(['1', '5', 1])
            client_1 = VKinderClient(users[0])
            assert client_1
            client_2 = VKinderClient(users[1])
            assert client_2

            self.db.save_client(client_1)
            self.db.save_client(client_2)

            for i in range(self.db.search_history_limit + 1):
                client_1.reset_search()
                client_2.reset_search()

                client_1.search.sex_id = randrange(0, 2, 1)
                client_1.search.status_id = randrange(1, 8, 1)
                client_1.search.city_id = 1
                client_1.search.city_name = 'Москва'
                client_1.search.min_age = randrange(0, 60, 1)
                client_1.search.max_age = randrange(client_1.search.min_age,
                                                    127, 1)
                client_1.rating_filter = 0

                client_2.search.sex_id = randrange(0, 2, 1)
                client_2.search.status_id = randrange(1, 8, 1)
                client_2.search.city_id = 2
                client_2.search.city_name = 'Санкт-Петербург'
                client_2.search.min_age = randrange(0, 60, 1)
                client_2.search.max_age = randrange(client_2.search.min_age,
                                                    127, 1)
                client_2.rating_filter = 0

                self.db.save_search(client_1)
                self.db.save_search(client_2)

                client_1.found_users = self.api.search_users()
                client_2.found_users = self.api.search_users(q='babych')
                assert len(client_1.found_users) > 0
                assert len(client_2.found_users) > 0

                self.db.save_users(client_1)
                self.db.save_users(client_2)