class MissingPartnerError(Exception):
    pass class PartnerObtainingError(Exception):
    pass class StrangerError(Exception):
    pass class StrangerServiceError(Exception):
    pass class UnknownCommandError(Exception):
    def __init__(self, command):
        super(UnknownCommandError, self).__init__()
        self.command = command class UnsupportedContentError(Exception):
    pass class Message:
    COMMAND_RE_PATTERN = re.compile(r'^/([a-z_]+)\b\s*(.*)$')

    def __init__(self, message_json):
        try:
            content_type, unused_chat_type, unused_chat_id = telepot.glance(message_json)
        except KeyError:
            raise UnsupportedContentError()
        if 'forward_from' in message_json:
            raise UnsupportedContentError()
        self.is_reply = 'reply_to_message' in message_json
        self.text = message_json.get('text')
        self.type = content_type
        self.command = None
        self.command_args = None
        self.sending_kwargs = {}

        try:
            init_method = getattr(self, '_init_' + content_type)
        except AttributeError:
            raise UnsupportedContentError()

        init_method(message_json)

    def decode_command_args(self):
        try:
            command_args = base64.urlsafe_b64decode(self.command_args)
        except (TypeError, ValueError) as err:
            raise UnsupportedContentError('Can\'t decode base 64') from err
        try:
            command_args = command_args.decode('utf-8')
        except UnicodeDecodeError as err:
            raise UnsupportedContentError('Can\'t decode UTF-8') from err
        try:
            command_args = json.loads(command_args)
        except (TypeError, ValueError) as err:
            raise UnsupportedContentError('Can\'t decode JSON') from err
        return command_args

    def _init_audio(self, message_json):
        try:
            audio = message_json['audio']
            self.sending_kwargs = {
                'audio': audio['file_id'],
                }
        except (KeyError, TypeError):
            raise UnsupportedContentError()
        self.sending_kwargs['duration'] = audio.get('duration')
        self.sending_kwargs['performer'] = audio.get('performer')
        self.sending_kwargs['title'] = audio.get('title')

    def _init_document(self, message_json):
        try:
            self.sending_kwargs = {
                'document': message_json['document']['file_id'],
                }
        except (KeyError, TypeError):
            raise UnsupportedContentError()

    def _init_location(self, message_json):
        try:
            location = message_json['location']
            self.sending_kwargs = {
                'latitude': location['latitude'],
                'longitude': location['longitude'],
                }
        except (KeyError, TypeError):
            raise UnsupportedContentError()

    def _init_photo(self, message_json):
        try:
            self.sending_kwargs = {
                'photo': message_json['photo'][0]['file_id'],
                }
        except (IndexError, KeyError, TypeError):
            raise UnsupportedContentError()
        self.sending_kwargs['caption'] = message_json.get('caption')

    def _init_sticker(self, message_json):
        try:
            self.sending_kwargs = {
                'sticker': message_json['sticker']['file_id'],
                }
        except (KeyError, TypeError):
            raise UnsupportedContentError()

    def _init_text(self, unused_message_json):
        self.sending_kwargs = {
            'text': self.text,
            }
        command_match = type(self).COMMAND_RE_PATTERN.match(self.text)
        if command_match:
            self.command = command_match.group(1)
            self.command_args = command_match.group(2)

    def _init_video(self, message_json):
        try:
            video = message_json['video']
            self.sending_kwargs = {
                'video': video['file_id'],
                }
        except (KeyError, TypeError):
            raise UnsupportedContentError()
        self.sending_kwargs['caption'] = message_json.get('caption')
        self.sending_kwargs['duration'] = video.get('duration')

    def _init_voice(self, message_json):
        try:
            voice = message_json['voice']
            self.sending_kwargs = {
                'voice': voice['file_id'],
                }
        except (KeyError, TypeError):
            raise UnsupportedContentError()
        self.sending_kwargs['duration'] = voice.get('duration') class StrangerSenderService:
    _instance = None

    def __init__(self, bot):
        self._bot = bot
        self._stranger_senders = {}

    @classmethod
    def get_instance(cls, bot=None):
        if cls._instance is None:
            if bot is None:
                raise StrangerSenderServiceError(
                    'Instance wasn\'t initialized. Provide arguments to construct one.',
                    )
            else:
                cls._instance = cls(bot)

        return cls._instance

    def get_cache_size(self):
        return len(self._stranger_senders)

    def get_or_create_stranger_sender(self, stranger):
        try:
            return self._stranger_senders[stranger.telegram_id]
        except KeyError:
            stranger_sender = StrangerSender(self._bot, stranger)
            self._stranger_senders[stranger.telegram_id] = stranger_sender
            return stranger_sender class StrangerService:
    def __init__(self):
        # We need to lock strangers for matching to prevent attempts to create
        # second conversation with single partner.
        self._locked_strangers_ids = set()
        self._strangers_cache = {}
        type(self)._instance = self

    @classmethod
    def get_instance(cls):
        try:
            return cls._instance
        except AttributeError:
            cls._instance = cls()
            return cls._instance

    @classmethod
    def get_full_strangers(cls):
        for stranger in Stranger.select():
            if stranger.is_full():
                yield stranger

    def get_cached_stranger(self, stranger):
        try:
            return self._strangers_cache[stranger.id]
        except KeyError:
            self._strangers_cache[stranger.id] = stranger

            if stranger.invited_by is not None:
                if stranger.invited_by.invited_by_id == stranger.id:
                    LOGGER.error(
                        'Circular reference between invited strangers %d and %d',
                        stranger.id,
                        stranger.invited_by_id,
                        )
                else:
                    stranger.invited_by = self.get_cached_stranger(stranger.invited_by)

            return stranger

    def get_cache_size(self):
        return len(self._strangers_cache)

    def get_or_create_stranger(self, telegram_id):
        try:
            try:
                stranger = Stranger.get(Stranger.telegram_id == telegram_id)
            except DoesNotExist:
                stranger = Stranger.create(
                    invitation=Stranger.get_invitation(),
                    telegram_id=telegram_id,
                    )
        except DatabaseError as err:
            raise StrangerServiceError('Database problems during `get_or_create_stranger`') from err

        return self.get_cached_stranger(stranger)

    def get_stranger(self, telegram_id):
        try:
            stranger = Stranger.get(Stranger.telegram_id == telegram_id)
        except (DatabaseError, DoesNotExist) as err:
            raise StrangerServiceError('Database problems during `get_stranger`') from err

        return self.get_cached_stranger(stranger)

    def get_stranger_by_invitation(self, invitation):
        if len(invitation) != INVITATION_LENGTH:
            raise StrangerServiceError(
                'Invitation length is wrong: \"{0}\"'.format(invitation),
                )

        try:
            stranger = Stranger.get(Stranger.invitation == invitation)
        except (DatabaseError, DoesNotExist) as err:
            raise StrangerServiceError('Database problems during `get_stranger_by_invitation`') \
                from err

        return self.get_cached_stranger(stranger)

    def _match_partner(self, stranger):
        """Tries to find a partner for obtained stranger.

        Raises:
            PartnerObtainingError: If there's no proper partner.

        Returns:
            Stranger
        """
        from .talk import Talk

        possible_partners = Stranger.select().where(
            Stranger.id != stranger.id,
            Stranger.looking_for_partner_from != None,
            )

        if stranger.sex == 'not_specified':
            possible_partners = possible_partners.where(Stranger.partner_sex == 'not_specified')
        else:
            possible_partners = possible_partners.where(
                (Stranger.partner_sex == stranger.sex) | (Stranger.partner_sex == 'not_specified'),
                )

        # If stranger wants to filter partners by sex, let's do that.
        if stranger.partner_sex == 'male' or stranger.partner_sex == 'female':
            possible_partners = possible_partners.where(
                Stranger.sex == stranger.partner_sex,
                )

        possible_partners = possible_partners.order_by(
            Stranger.bonus_count.desc(),
            Stranger.looking_for_partner_from,
            )

        last_partners_ids = frozenset(Talk.get_last_partners_ids(stranger))

        partner = None
        partner_language_priority = 1000
        for possible_partner in possible_partners:
            if possible_partner.id in last_partners_ids or \
                    possible_partner.id in self._locked_strangers_ids:
                continue
            for priority, language in enumerate(
                    stranger.get_languages()[:partner_language_priority],
                ):
                if possible_partner.speaks_on_language(language):
                    partner = possible_partner
                    partner_language_priority = priority
                    if priority == 0:
                        break
            else:
                continue
            break
        if partner is None:
            raise PartnerObtainingError()

        self._locked_strangers_ids.add(partner.id)
        return self.get_cached_stranger(partner)

    async def match_partner(self, stranger):
        """Finds partner for the stranger. Does handling of strangers who have blocked the bot.

        Raises:
            PartnerObtainingError: If there's no proper partners.
            StrangerServiceError: If the stranger has blocked the bot.
        """
        while True:
            partner = self._match_partner(stranger)

            try:
                await partner.notify_partner_found(stranger)
            except StrangerError as err:
                # Potential partner has blocked the bot. Let's look for next
                # potential partner.
                LOGGER.info('Bad potential partner for %d. %s', stranger.id, err)
                await partner.end_talk()
                self._locked_strangers_ids.discard(partner.id)
                continue

            break

        try:
            await stranger.notify_partner_found(partner)
        except StrangerError as err:
            self._locked_strangers_ids.discard(partner.id)
            # Stranger has blocked the bot.
            raise StrangerServiceError('Can\'t notify seeking for partner stranger') from err

        await stranger.set_partner(partner)
        self._locked_strangers_ids.discard(partner.id)
        LOGGER.debug('Found partner: %d -> %d.', stranger.id, partner.id) class StrangerSetupWizard(Wizard):
    """Wizard which guides stranger through process of customizing her parameters. Activates
    automatically for novices.
    """

    def __init__(self, stranger):
        super(StrangerSetupWizard, self).__init__()
        self._stranger = stranger
        self._sender = StrangerSenderService.get_instance().get_or_create_stranger_sender(stranger)

    async def activate(self):
        self._stranger.wizard = 'setup'
        self._stranger.wizard_step = 'languages'
        self._stranger.save()
        await self._prompt()

    async def deactivate(self):
        self._stranger.wizard = 'none'
        self._stranger.wizard_step = None
        self._stranger.save()
        try:
            await self._sender.send_notification(
                _(
                    'Thank you. Use /begin to start looking for a conversational partner,'
                    ' once you\'re matched you can use /end to end the conversation.',
                    ),
                reply_markup={'hide_keyboard': True},
                )
        except TelegramError as err:
            LOGGER.warning('Deactivate. Can\'t notify stranger. %s', err)

    async def handle(self, message):
        """Returns:
            bool: `True` if message was interpreted in this method. `False` if message still needs
                interpretation.
        """
        if self._stranger.wizard == 'none': # Wizard isn't active. Check if we should activate it.
            if self._stranger.is_novice():
                await self.activate()
                return True

            return False
        elif self._stranger.wizard != 'setup':
            return False

        try:
            if self._stranger.wizard_step == 'languages':
                try:
                    self._stranger.set_languages(get_languages_codes(message.text))
                except EmptyLanguagesError as err:
                    await self._sender.send_notification(
                        _('Please specify at least one language.'),
                        )
                except LanguageNotFoundError as err:
                    LOGGER.info('Languages weren\'t parsed: \"%s\"', message.text)
                    await self._sender.send_notification(
                        _('Language \"{0}\" wasn\'t found.'),
                        err.name,
                        )
                except StrangerError:
                    LOGGER.info('Too much languages were specified: \"%s\"', message.text)
                    await self._sender.send_notification(
                        _(
                            'Too much languages were specified. Please shorten your list'
                            ' to 6 languages.',
                            ),
                        )
                else:
                    self._sender.update_translation()
                    self._stranger.wizard_step = 'sex'
                    self._stranger.save()

                await self._prompt()
            elif self._stranger.wizard_step == 'sex':
                try:
                    self._stranger.set_sex(message.text)
                except SexError as err:
                    LOGGER.info('Stranger\'s sex wasn\'t parsed: \"%s\"', message.text)
                    await self._sender.send_notification(
                        _('Unknown sex: \"{0}\" -- is not a valid sex name.'),
                        err.name,
                        )
                    await self._prompt()
                else:
                    if self._stranger.sex == 'not_specified':
                        self._stranger.partner_sex = 'not_specified'
                        # Calls Stranger.save() inside.
                        await self.deactivate()
                    else:
                        self._stranger.wizard_step = 'partner_sex'
                        self._stranger.save()
                        await self._prompt()
            elif self._stranger.wizard_step == 'partner_sex':
                try:
                    self._stranger.set_partner_sex(message.text)
                except SexError as err:
                    LOGGER.info('Stranger partner\'s sex wasn\'t parsed: \"%s\"', message.text)
                    await self._sender.send_notification(
                        _('Unknown sex: \"{0}\" -- is not a valid sex name.'),
                        err.name,
                        )
                    await self._prompt()
                else:
                    # Calls Stranger.save() inside.
                    await self.deactivate()
            else:
                LOGGER.warning(
                    'Undknown wizard_step value was found: \"%s\"',
                    self._stranger.wizard_step,
                )
        except TelegramError as err:
            LOGGER.warning('handle() Can not notify stranger. %s', err)

        return True

    async def handle_command(self, message):
        """Returns:
            bool: `True` if command was interpreted in this method. `False` if command still needs
                interpretation.
        """
        if self._stranger.wizard == 'none':
            # Wizard isn't active. Check if we should activate it.
            return (await self.handle(message)) and message.command != 'start'
        elif self._stranger.is_full():
            if self._stranger.wizard == 'setup':
                await self.deactivate()
            return False
        else:
            try:
                await self._sender.send_notification(
                    _('Finish setup process please. After that you can start using bot.'),
                    )
            except TelegramError as err:
                LOGGER.warning('Handle command. Cant notify stranger. %s', err)
            await self._prompt()
            return True

    async def _prompt(self):
        wizard_step = self._stranger.wizard_step

        try:
            if wizard_step == 'languages':
                languages = self._stranger.get_languages()
                # Just split languages by pairs.
                keyboard = [
                    SUPPORTED_LANGUAGES_NAMES[i: i + 2]
                    for i in range(0, len(SUPPORTED_LANGUAGES_NAMES), 2)
                    ]

                try:
                    languages_enumeration = get_languages_names(languages)
                except LanguageNotFoundError:
                    LOGGER.error('Language not found at setup wizard: %s', self._stranger.languages)
                    languages_enumeration = ''

                if languages and languages_enumeration:
                    if len(languages) == 1:
                        keyboard.append([_('Leave the language unchanged')])
                        prompt = _(
                            'Your current language is {0}. Enumerate the languages'
                            ' you speak like this: \"English, Italian\" -- in descending order'
                            ' of your speaking convenience or just pick one at special keyboard.',
                            )
                    else:
                        keyboard.append([_('Leave the languages unchanged')])
                        prompt = _(
                            'Your current languages are: {0}. Enumerate the languages you'
                            ' speak the same way -- in descending order of your speaking'
                            ' convenience or just pick one at special keyboard.',
                            )
                else:
                    prompt = _(
                        'Enumerate the languages you speak like this: \"English, Italian\"'
                        ' -- in descending order of your speaking convenience or just pick one'
                        ' at special keyboard.'
                        )

                LOGGER.debug(
                    'Sending languages setup notification `%s`. Keyboard: %s',
                    prompt,
                    keyboard,
                    )
                await self._sender.send_notification(
                    prompt,
                    languages_enumeration,
                    reply_markup={
                        'keyboard': keyboard,
                        },
                    )
            elif wizard_step == 'sex':
                await self._sender.send_notification(
                    _(
                        'Set up your sex. If you pick \"Not Specified\" you can\'t choose'
                        ' your partner\'s sex.',
                        ),
                    reply_markup=SEX_KEYBOARD,
                    )
            elif wizard_step == 'partner_sex':
                await self._sender.send_notification(
                    _('Choose your partner\'s sex'),
                    reply_markup=SEX_KEYBOARD,
                    )
        except TelegramError as err:
            LOGGER.warning('_prompt() Can\'t notify stranger. %s', err) RANDTALKBOT_DIR = path.abspath(path.dirname(__file__))
LOCALE_DIR = path.join(RANDTALKBOT_DIR, 'locale')
import datetime
import logging
import sys
import telepot
import telepot.aio
from telepot.exception import TelegramError
from .errors import MissingPartnerError, PartnerObtainingError, \
    StrangerError, StrangerServiceError, UnknownCommandError, UnsupportedContentError
from .message import Message
from .stranger_sender_service import StrangerSenderService
from .stranger_service import StrangerService
from .stranger_setup_wizard import StrangerSetupWizard
from .utils import __version__
# RandTalkBot Bot matching you with a random person on Telegram.
# Copyright (C) 2016 quasiyoke
#
# You should have received a copy of the GNU Affero General Public License
# along with this program.  If not, see <http://www.gnu.org/licenses/>.


LOGGER = logging.getLogger('randtalkbot.stranger_handler')


def _(string_instance):
    return string_instance


class StrangerHandler(telepot.aio.helper.UserHandler):
    HOUR_TIMEDELTA = datetime.timedelta(hours=1)
    LONG_WAITING_TIMEDELTA = datetime.timedelta(minutes=10)
    COORDINATOR_CLASS = telepot.aio.helper.CallbackQueryCoordinator

    def __init__(self, seed_tuple, *args, **kwargs):
        super(StrangerHandler, self).__init__(seed_tuple, *args, **kwargs)
        bot, initial_msg, unused_seed = seed_tuple
        self._from_id = initial_msg['from']['id']

        try:
            self._stranger = StrangerService.get_instance() \
                .get_or_create_stranger(self._from_id)
        except StrangerServiceError as err:
            LOGGER.exception('Problems with StrangerHandler construction')
            sys.exit(f'Problems with StrangerHandler construction. {err}')

        self._sender = StrangerSenderService.get_instance(bot) \
            .get_or_create_stranger_sender(self._stranger)

        self._stranger_setup_wizard = StrangerSetupWizard(self._stranger)