Ejemplo n.º 1
0
class Sender():
    def __init__(self, email: str) -> None:
        self.failed_email_user_id = 0
        self.bot_email = email
        self.queue_from_bot = email
        self.applicant = Applicant()
        self.loop = asyncio.get_event_loop()
        self.current_appeal: Optional[dict] = None
        self.stop_timer = Timer(self.stop_appeal_sending, self.loop)
        self.captcha_solver = CaptchaSolver()
        self.user_captcha_text: Optional[str] = None

    def sending_in_progress(self):
        return self.current_appeal is not None

    def send_to_bot(self) -> HttpRabbit:
        return HttpRabbit()

    async def process_new_appeal(self,
                                 message: aio_pika.IncomingMessage) -> None:
        success = True
        appeal = json.loads(message.body.decode("utf-8"))
        self.convert_recipient(appeal['appeal'])

        try:
            success = await self.async_process_new_appeal(appeal)
        except Exception:
            logger.exception('Что-то пошло не так')
            success = False

        if success:
            self.current_appeal = None
            self.failed_email_user_id = 0
            await message.ack()

            logger.info(f'Обращение обработано ' +
                        f'user_id: {appeal["user_id"]} ' +
                        f'appeal_id: {appeal["appeal_id"]}')
        else:
            logger.info('Попробуем отправить еще раз')
            await self.process_new_appeal(message)

    def convert_recipient(self, appeal: dict) -> None:
        department = appeal['police_department']
        appeal['police_subdepartment'] = None

        if department in config.DEPARTMENT_NAMES:
            appeal['police_department'] = config.DEPARTMENT_NAMES[department]
        elif department in config.MINSK_DEPARTMENT_NAMES:
            appeal['police_department'] = config.DEPARTMENT_NAMES[config.MINSK]

            appeal['police_subdepartment'] = \
                config.MINSK_DEPARTMENT_NAMES[department]

    async def solve_captcha(self, browser: webdriver.Remote) -> bool:
        appeal: dict = self.current_appeal or dict()
        email: str = self.get_value(appeal, 'sender_email', self.bot_email)

        if self.failed_email_user_id == appeal['user_id']:
            email = self.bot_email
            self.current_appeal['sender_email'] = None
            self.current_appeal['sender_email_password'] = ''

        logger.info(f"Достали имейл: {email}")

        proceed, success, captcha_text = await self.get_captcha_text(appeal,
                                                                     email,
                                                                     browser)

        if not proceed:
            return success

        proceed = await self.process_captcha(captcha_text,
                                             appeal['user_id'],
                                             appeal['appeal_id'],
                                             browser,
                                             silent=True)
        return proceed

    async def async_process_new_appeal(self, appeal: dict) -> bool:
        logger.info(f'Новое обращение: {pformat(appeal)}')
        self.current_appeal = appeal

        with brwsr.get_browser() as browser:
            if not await self.solve_captcha(browser):
                return False

            proceed, url = await self.get_appeal_url()

            if not proceed:
                return False

            return await self.send_appeal(url, browser)

    async def get_captcha_text(
            self,
            appeal: dict,
            email: str,
            browser: webdriver.Remote) -> Tuple[bool, bool, str]:
        try:
            captcha_solution = await self.recognize_captcha(email, browser)

            if captcha_solution is None:
                logger.info("Капча не распозналась =(")
                await self.send_captcha(appeal['appeal_id'],
                                        appeal['user_id'],
                                        email,
                                        browser)

                cancel, captcha_solution = \
                    await self.wait_for_input_or_cancel()

                if cancel:
                    return False, True, ''
            else:
                logger.info("Капча распозналась.")

            return True, True, captcha_solution
        except BrowserError:
            logger.info("Фейл браузинга")
            return False, False, ''
        except Exception:
            logger.exception('ОЙ get_captcha_text')
            return False, False, ''

    async def wait_for_input_or_cancel(self) -> Tuple[bool, str]:
        while True:
            if not self.sending_in_progress():
                return True, ''

            if self.user_captcha_text:
                text = self.user_captcha_text
                self.user_captcha_text = None
                return False, text

            await asyncio.sleep(2)

    async def send_captcha(self,
                           appeal_id: int,
                           user_id: int,
                           email: str,
                           browser: webdriver.Remote) -> None:
        captcha = self.applicant.get_png_captcha(email, browser)

        self.stop_timer.cock_it(config.CANCEL_TIMEOUT)

        try:
            await self.send_to_bot().send_captcha_url(captcha,
                                                      appeal_id,
                                                      user_id,
                                                      self.queue_from_bot)
        except ErrorWhilePutInQueue as exc:
            logger.error(exc.text)
            if exc.data:
                await self.send_to_bot().do_request(exc.data[0], exc.data[1])

    async def recognize_captcha(self,
                                email: str,
                                browser: webdriver.Remote) -> Optional[str]:
        svg_captcha = self.applicant.get_svg_captcha(email, browser)
        return await self.captcha_solver.solve(svg_captcha)

    async def process_bot_message(self,
                                  message: aio_pika.IncomingMessage) -> None:
        data = json.loads(message.body)
        logger.info(f'Сообщение от бота: {data}')
        email = self.get_value(data, 'sender_email', self.bot_email)
        logger.info(f"Достали имейл: {email}")
        user_id = data['user_id']
        appeal_id = data['appeal_id']

        # костыльчик пока бот не научится не хранить обращения, а парсить
        # бот уже научился, но убирать страшно, такие дела
        if not self.current_appeal \
                or self.current_appeal['user_id'] != user_id \
                or self.current_appeal['appeal_id'] != appeal_id:
            return

        if data['type'] == config.CAPTCHA_TEXT:
            self.stop_timer.delete()
            self.user_captcha_text = data['captcha_text']

        elif data['type'] == config.CANCEL:
            await self.stop_appeal_sending(local=True)

    async def process_captcha(self,
                              captcha_text: str,
                              user_id: int,
                              appeal_id: int,
                              browser: webdriver.Remote,
                              silent=False) -> bool:
        if self.applicant.enter_captcha_and_submit(
                captcha_text, browser) != config.OK:
            logger.info("Капча не подошла")
            return False

        if not silent:
            await self.send_to_bot().send_status(user_id,
                                                 config.CAPTCHA_OK,
                                                 self.queue_from_bot,
                                                 appeal_id,
                                                 self.current_appeal['appeal'])
        return True

    async def send_appeal(self, url: str, browser: webdriver.Remote) -> bool:
        try:
            status_code, message = self.applicant.send_appeal(
                self.current_appeal['appeal'], url, browser)

            if status_code != config.OK:
                logger.info(f"Ошибка при отправке - {message}")
                return False
        except BrowserError:
            logger.info("Фейл браузинга")
            return False
        except ErrorWhilePutInQueue as exc:
            logger.error(exc.text)
            if exc.data:
                await self.send_to_bot().do_request(exc.data[0], exc.data[1])
        except RancidAppeal:
            logger.info("Взяли протухшую форму обращения")
            proceed, url = await self.get_appeal_url()

            if not proceed:
                return False

            return await self.send_appeal(url, browser)
        except Exception:
            logger.exception('ОЙ send_appeal')
            return False

        await self.send_to_bot().send_status(self.current_appeal['user_id'],
                                             config.OK,
                                             self.queue_from_bot,
                                             self.current_appeal['appeal_id'],
                                             self.current_appeal['appeal'])
        return True

    async def get_appeal_url(self) -> Tuple[bool, str]:
        email = self.get_value(self.current_appeal, 'sender_email', None)

        password = self.get_value(self.current_appeal,
                                  'sender_email_password',
                                  config.EMAIL_PWD)

        if not email:
            email = self.bot_email
            password = config.EMAIL_PWD

        try:
            url = self.applicant.get_appeal_url(email, password)
            return True, url
        except NoMessageFromPolice:
            logger.info("Фейл почты. Не нашлось письмо.")
            return False, ''
        except AppealURLParsingFailed:
            logger.info("Не удалось распарсить урл из письма.")
            return False, ''
        except EmailLoginError as exc:
            logger.info(f'Не могу залогиниться {exc}')
            await self.maybe_tell_user_about_broken_email(email)
            return False, ''

    async def maybe_tell_user_about_broken_email(self, email: str):
        if email == self.bot_email:
            return

        self.failed_email_user_id = self.current_appeal['user_id']

        await self.send_to_bot().send_status(self.current_appeal['user_id'],
                                             config.BAD_EMAIL,
                                             self.queue_from_bot,
                                             self.current_appeal['appeal_id'],
                                             self.current_appeal['appeal'])

    async def start_sender(self, loop: AbstractEventLoop) -> None:
        appeals = rabbit_amqp.Rabbit(config.RABBIT_EXCHANGE_MANAGING,
                                     config.RABBIT_QUEUE_APPEAL,
                                     config.RABBIT_AMQP_ADDRESS,
                                     loop,
                                     "appeals")

        bot = rabbit_amqp.Rabbit(config.RABBIT_EXCHANGE_SENDING,
                                 self.queue_from_bot,
                                 config.RABBIT_AMQP_ADDRESS,
                                 loop,
                                 "bot_messages")

        asyncio.gather(bot.start(self.process_bot_message, passive=False),
                       appeals.start(self.process_new_appeal, passive=True),
                       self.stop_timer.start())

        logger.info(f"Воркер стартует.")

    async def stop_appeal_sending(self, local=False):
        logger.info(f"Останавливаем отправку обращения")
        if not local:
            await self.send_to_bot().send_sending_stopped(
                self.current_appeal['appeal_id'],
                self.current_appeal['user_id'],
                self.queue_from_bot
            )

        self.current_appeal = None
        self.user_captcha_text = None
        self.stop_timer.delete()
        logger.info("Отмена")

    def start(self):
        self.loop.run_until_complete(self.start_sender(self.loop))
        self.loop.run_forever()

    @classmethod
    def get_value(cls,
                  data: Optional[dict],
                  key: str,
                  default: Any = None) -> Any:
        try:
            value = data[key]

            if value:
                return value
            elif default:
                return default
            else:
                return value
        except KeyError:
            if default:
                return default
            else:
                return None

    def stop(self):
        logger.info('Суецыд')