Exemplo n.º 1
0
def intro_handler(bot: Bot, update: Update):
    #chat_id = update.message.chat_id
    #p, _ = Profile.objects.get_or_create(
    #	external_id = chat_id,
    ###	}
    #)
    intro = update.callback_query.data
    j = JobQueue()
    j.set_dispatcher(dispatcher)
    if intro == CALLBACK_BUTTON1_LEFT:
        chat_id = update.effective_message.chat_id
        text = "Прекрасно! Теперь я буду каждый день отправлять тебе упражнения. Береги глаза друг!"
        j.run_repeating(callback_alarm, 60 * 60 * 24, 60, context=chat_id)
        j.start()
        update.effective_message.reply_text(
            text=text,
            reply_markup=get_keyboard2(),
        )
        return ConversationHandler.END
    elif intro == CALLBACK_BUTTON2_RIGHT:
        text = "Тогда ты можешь просто насладиться статьями, которые будут только улучшаться"
        update.callback_query.message.reply_text(
            text=text,
            reply_markup=get_keyboard2(),
        )
        return ConversationHandler.END
Exemplo n.º 2
0
def connect_bot():
    """
    Make new bot with default parse mode to HTML and disabled web page preview
    Make Dispatcher with PGPersistence and set JobQueue

    Return value: (dispatcher, bot, job_queue)
    """
    logger.info('Connecting bot...')
    new_bot = Bot(token=config.BOT_TOKEN,
                  defaults=Defaults(
                      disable_web_page_preview=True,
                      parse_mode=ParseMode.HTML,
                  ))
    jq = JobQueue()
    persistence = PGPersistence()
    dp = Dispatcher(
        bot=new_bot,
        update_queue=None,
        use_context=True,
        job_queue=jq,
        persistence=persistence,
    )
    jq.set_dispatcher(dp)
    jq.start()
    logger.info('Bot connected successfully')
    return dp, new_bot, jq
Exemplo n.º 3
0
	def __init__(
		self,
		token: str = None,
		workers: int = 4,
	):
		"""
		Initialize the updater.
		"""
		con_pool_size = workers + 4
		request_kwargs = {'con_pool_size': con_pool_size}
		self._request = Request(**request_kwargs)
		self.bot = Bot(token, request=self._request)

		update_queue: Queue = Queue()
		job_queue = JobQueue()
		exception_event = Event()
		dispatcher = HomeBotDispatcher(
			self.bot,
			update_queue,
			job_queue=job_queue,
			workers=workers,
			exception_event=exception_event
		)
		job_queue.set_dispatcher(dispatcher)

		super().__init__(dispatcher=dispatcher, workers=None)
Exemplo n.º 4
0
def jq(cdp):
    jq = JobQueue()
    jq.set_dispatcher(cdp)
    job_store = PTBSQLAlchemyJobStore(dispatcher=cdp, url="sqlite:///:memory:")
    jq.scheduler.add_jobstore(job_store)
    jq.start()
    yield jq
    jq.stop()
Exemplo n.º 5
0
def main():
    mongoengine.connect(db=config.database.db, host=config.database.host,
                        port=config.database.port,
                        username=config.database.user,
                        password=config.database.password)

    start_handler = CommandHandler('start', commands)
    dispatcher.add_handler(start_handler)

    help_handler = CommandHandler('help', commands)
    dispatcher.add_handler(help_handler)

    register_handler = CommandHandler('register', register)
    dispatcher.add_handler(register_handler)

    info_handler = CommandHandler('info', info)
    dispatcher.add_handler(info_handler)

    balance_handler = CommandHandler('balance', balance)
    dispatcher.add_handler(balance_handler)

    balance_prefix_handler = PrefixHandler(COMMAND_PREFIX, 'balance', balance)
    dispatcher.add_handler(balance_prefix_handler)

    withdraw_handler = CommandHandler('withdraw', withdraw)
    dispatcher.add_handler(withdraw_handler)

    transfer_handler = CommandHandler('transfer', transfer)
    dispatcher.add_handler(transfer_handler)

    transfer_prefix_handler = PrefixHandler(COMMAND_PREFIX, 'transfer',
                                            transfer)
    dispatcher.add_handler(transfer_prefix_handler)

    tip_handler = CommandHandler('tip', tip)
    dispatcher.add_handler(tip_handler)

    tip_prefix_handler = PrefixHandler(COMMAND_PREFIX, 'tip', tip)
    dispatcher.add_handler(tip_prefix_handler)

    outputs_handler = CommandHandler('outputs', outputs)
    dispatcher.add_handler(outputs_handler)

    optimize_handler = CommandHandler('optimize', optimize)
    dispatcher.add_handler(optimize_handler)

    dispatcher.add_error_handler(handle_errors)

    jobqueue = JobQueue()
    jobqueue.set_dispatcher(dispatcher)
    jobqueue.run_repeating(update_balance_wallets,
                           config.wallet_balance_update_interval)
    jobqueue.start()

    updater.start_polling()
Exemplo n.º 6
0
 def test_job_run(self, _dp, use_context):
     _dp.use_context = use_context
     job_queue = JobQueue()
     job_queue.set_dispatcher(_dp)
     if use_context:
         job = job_queue.run_repeating(self.job_context_based_callback, 0.02, context=2)
     else:
         job = job_queue.run_repeating(self.job_run_once, 0.02, context=2)
     assert self.result == 0
     job.run(_dp)
     assert self.result == 1
Exemplo n.º 7
0
def main():
    updater = Updater(token=FreeOnEpic.bot_token, use_context=True)
    dp = updater.dispatcher
    job_queue = JobQueue()
    job_queue.set_dispatcher(dp)
    job_queue.run_repeating(callback=FreeOnEpic.get_links, interval=600)
    logger.info('The bot has started')
    dp.add_handler(CommandHandler('freegame', FreeOnEpic.free_game))
    dp.add_handler(CommandHandler("help", FreeOnEpic.help_command))
    job_queue.start()
    updater.start_polling()
    updater.idle()
Exemplo n.º 8
0
def main():
    """Start the bot."""
    # Create the Updater and pass it your bot's token.
    # Make sure to set use_context=True to use the new context based callbacks
    # Post version 12 this will no longer be necessary
    updater = Updater(config.bottoken, use_context=True)

    # Get the dispatcher to register handlers
    dp = updater.dispatcher

    # on different commands - answer in Telegram
    # dp.add_handler(CommandHandler("start", start))
    dp.add_handler(CommandHandler("hilfe", help))
    dp.add_handler(CommandHandler("status", status))
    dp.add_handler(CommandHandler("forceupdate", force_update))

    # Add conversation handler with the states ASKFORLK, UNIQUELK, MULTIPLELK
    conv_handler = ConversationHandler(
        entry_points=[
            CommandHandler('start', start),
            CommandHandler('neuerlk', newlk),
            CommandHandler('entfernelk', removelk)
        ],
        states={
            ASKFORLK: [MessageHandler(Filters.text, ask_for_landkreis)],
            CHOOSELK: [MessageHandler(Filters.text, choose_landkreis)],
            REMOVELK: [MessageHandler(Filters.text, remove_landkreis)]
        },
        fallbacks=[CommandHandler('cancel', cancel)])
    dp.add_handler(conv_handler)

    # Error handler
    dp.add_error_handler(error)

    # Read the existing data
    casesdata.load_data()

    # Start the Bot
    updater.start_polling()

    # Check for updates of rki numbers and notify users every hour (3600s).
    cronjob = JobQueue()
    cronjob.set_dispatcher(dp)
    cronjob.run_repeating(process_case_updates, 3600)
    cronjob.start()

    # Run the bot until you press Ctrl-C or the process receives SIGINT,
    # SIGTERM or SIGABRT. This should be used most of the time, since
    # start_polling() is non-blocking and will stop the bot gracefully.
    updater.idle()
Exemplo n.º 9
0
    def test_default_tzinfo(self, _dp, tz_bot):
        # we're parametrizing this with two different UTC offsets to exclude the possibility
        # of an xpass when the test is run in a timezone with the same UTC offset
        jq = JobQueue()
        original_bot = _dp.bot
        _dp.bot = tz_bot
        jq.set_dispatcher(_dp)
        try:
            jq.start()

            when = dtm.datetime.now(tz_bot.defaults.tzinfo) + dtm.timedelta(seconds=0.0005)
            jq.run_once(self.job_run_once, when.time())
            sleep(0.001)
            assert self.result == 1

            jq.stop()
        finally:
            _dp.bot = original_bot
Exemplo n.º 10
0
    def __init__(self, logger):
        self.logger = logger

        self.settings = default_settings

        self.token = os.getenv('tbot_api')
        self.spc_url = os.getenv('spc_url')
        self.spc_wallet = os.getenv('spc_wallet')
        self.admin_id = int(os.getenv('spc_admin_id', None))

        self._create_tables()

        self.delays = {}
        self.latest_dices = {}
        self.roll_messages = []
        self.secret_key, self.public_key = self._get_keys(self.spc_wallet)
        self.bot = Bot(self.token)
        self.updater = Updater(self.token, use_context=True)

        msg_filter = Filters.text \
                     & (~Filters.forwarded) \
                     & (~Filters.update.edited_message)

        dice_filter = Filters.dice \
                      & (~Filters.forwarded) \
                      & (~Filters.update.edited_message) \
                      & Filters.group

        msg_handler = MessageHandler(filters=msg_filter,
                                     callback=self.message_callback)
        dice_handler = MessageHandler(filters=dice_filter,
                                      callback=self.dice_callback)
        self.updater.dispatcher.add_handler(msg_handler)
        self.updater.dispatcher.add_handler(dice_handler)

        jobs = JobQueue()
        jobs.set_dispatcher(self.updater.dispatcher)
        jobs.run_repeating(callback=self.job_callback, interval=10)
        jobs.run_repeating(callback=self.message_job_callback, interval=1)
        jobs.start()

        self.updater.start_polling()
        self.updater.idle()
Exemplo n.º 11
0
    def bot_command(self):
        """
        creating the bot instructions
        """
        updater = Updater(self._token, use_context=True)
        dp = updater.dispatcher
        job_queue = JobQueue()
        job_queue.set_dispatcher(dp)
        # repeats the command on repeat
        job_queue.run_repeating(self.bot_audio_repeat,
                                interval=self.interval *
                                60)  # repeated intervals
        # creates the handle for the command
        dp.add_handler(
            CommandHandler(self.command_name,
                           self.bot_audio_on_command))  # command

        updater.start_polling()
        job_queue.start()
        updater.idle()
Exemplo n.º 12
0
    def handle(self, *args, **options):
        request = Request(connect_timeout=5, read_timeout=5, con_pool_size=8)
        bot = Bot(request=request, token=config.bot_token)
        print(bot.get_me())

        job_queue = JobQueue()
        updater = Updater(bot=bot, use_context=True)

        job_queue.set_dispatcher(updater.dispatcher)
        job_queue.run_repeating(upload_hot_video, 30, name="hot")
        job_queue.run_repeating(setup_schedule, 30, name="schedule_setup")
        job_queue.start()

        message_handler = MessageHandler(Filters.text, do_echo)
        updater.dispatcher.add_handler(CommandHandler("help", help_command))
        updater.dispatcher.add_handler(CommandHandler("send", send_post))
        updater.dispatcher.add_handler(CommandHandler("set", job_maker))
        updater.dispatcher.add_handler(CommandHandler("unset", unset))
        updater.dispatcher.add_handler(message_handler)
        updater.start_polling()
        updater.idle()
Exemplo n.º 13
0
def job_queue(bot, _dp):
    jq = JobQueue()
    jq.set_dispatcher(_dp)
    jq.start()
    yield jq
    jq.stop()
Exemplo n.º 14
0
class Updater:
    """
    This class, which employs the :class:`telegram.ext.Dispatcher`, provides a frontend to
    :class:`telegram.Bot` to the programmer, so they can focus on coding the bot. Its purpose is to
    receive the updates from Telegram and to deliver them to said dispatcher. It also runs in a
    separate thread, so the user can interact with the bot, for example on the command line. The
    dispatcher supports handlers for different kinds of data: Updates from Telegram, basic text
    commands and even arbitrary types. The updater can be started as a polling service or, for
    production, use a webhook to receive updates. This is achieved using the WebhookServer and
    WebhookHandler classes.


    Attributes:
        bot (:class:`telegram.Bot`): The bot used with this Updater.
        user_sig_handler (:obj:`function`): Optional. Function to be called when a signal is
            received.
        update_queue (:obj:`Queue`): Queue for the updates.
        job_queue (:class:`telegram.ext.JobQueue`): Jobqueue for the updater.
        dispatcher (:class:`telegram.ext.Dispatcher`): Dispatcher that handles the updates and
            dispatches them to the handlers.
        running (:obj:`bool`): Indicates if the updater is running.
        persistence (:class:`telegram.ext.BasePersistence`): Optional. The persistence class to
            store data that should be persistent over restarts.
        use_context (:obj:`bool`): Optional. :obj:`True` if using context based callbacks.

    Args:
        token (:obj:`str`, optional): The bot's token given by the @BotFather.
        base_url (:obj:`str`, optional): Base_url for the bot.
        base_file_url (:obj:`str`, optional): Base_file_url for the bot.
        workers (:obj:`int`, optional): Amount of threads in the thread pool for functions
            decorated with ``@run_async`` (ignored if `dispatcher` argument is used).
        bot (:class:`telegram.Bot`, optional): A pre-initialized bot instance (ignored if
            `dispatcher` argument is used). If a pre-initialized bot is used, it is the user's
            responsibility to create it using a `Request` instance with a large enough connection
            pool.
        dispatcher (:class:`telegram.ext.Dispatcher`, optional): A pre-initialized dispatcher
            instance. If a pre-initialized dispatcher is used, it is the user's responsibility to
            create it with proper arguments.
        private_key (:obj:`bytes`, optional): Private key for decryption of telegram passport data.
        private_key_password (:obj:`bytes`, optional): Password for above private key.
        user_sig_handler (:obj:`function`, optional): Takes ``signum, frame`` as positional
            arguments. This will be called when a signal is received, defaults are (SIGINT,
            SIGTERM, SIGABRT) settable with :attr:`idle`.
        request_kwargs (:obj:`dict`, optional): Keyword args to control the creation of a
            `telegram.utils.request.Request` object (ignored if `bot` or `dispatcher` argument is
            used). The request_kwargs are very useful for the advanced users who would like to
            control the default timeouts and/or control the proxy used for http communication.
        use_context (:obj:`bool`, optional): If set to :obj:`True` uses the context based callback
            API (ignored if `dispatcher` argument is used). Defaults to :obj:`True`.
            **New users**: set this to :obj:`True`.
        persistence (:class:`telegram.ext.BasePersistence`, optional): The persistence class to
            store data that should be persistent over restarts (ignored if `dispatcher` argument is
            used).
        defaults (:class:`telegram.ext.Defaults`, optional): An object containing default values to
            be used if not set explicitly in the bot methods.

    Note:
        * You must supply either a :attr:`bot` or a :attr:`token` argument.
        * If you supply a :attr:`bot`, you will need to pass :attr:`defaults` to *both* the bot and
          the :class:`telegram.ext.Updater`.

    Raises:
        ValueError: If both :attr:`token` and :attr:`bot` are passed or none of them.

    """

    _request = None

    def __init__(
        self,
        token: str = None,
        base_url: str = None,
        workers: int = 4,
        bot: Bot = None,
        private_key: bytes = None,
        private_key_password: bytes = None,
        user_sig_handler: Callable = None,
        request_kwargs: Dict[str, Any] = None,
        persistence: 'BasePersistence' = None,
        defaults: 'Defaults' = None,
        use_context: bool = True,
        dispatcher: Dispatcher = None,
        base_file_url: str = None,
    ):

        if defaults and bot:
            warnings.warn(
                'Passing defaults to an Updater has no effect when a Bot is passed '
                'as well. Pass them to the Bot instead.',
                TelegramDeprecationWarning,
                stacklevel=2,
            )

        if dispatcher is None:
            if (token is None) and (bot is None):
                raise ValueError('`token` or `bot` must be passed')
            if (token is not None) and (bot is not None):
                raise ValueError('`token` and `bot` are mutually exclusive')
            if (private_key is not None) and (bot is not None):
                raise ValueError('`bot` and `private_key` are mutually exclusive')
        else:
            if bot is not None:
                raise ValueError('`dispatcher` and `bot` are mutually exclusive')
            if persistence is not None:
                raise ValueError('`dispatcher` and `persistence` are mutually exclusive')
            if workers is not None:
                raise ValueError('`dispatcher` and `workers` are mutually exclusive')
            if use_context != dispatcher.use_context:
                raise ValueError('`dispatcher` and `use_context` are mutually exclusive')

        self.logger = logging.getLogger(__name__)

        if dispatcher is None:
            con_pool_size = workers + 4

            if bot is not None:
                self.bot = bot
                if bot.request.con_pool_size < con_pool_size:
                    self.logger.warning(
                        'Connection pool of Request object is smaller than optimal value (%s)',
                        con_pool_size,
                    )
            else:
                # we need a connection pool the size of:
                # * for each of the workers
                # * 1 for Dispatcher
                # * 1 for polling Updater (even if webhook is used, we can spare a connection)
                # * 1 for JobQueue
                # * 1 for main thread
                if request_kwargs is None:
                    request_kwargs = {}
                if 'con_pool_size' not in request_kwargs:
                    request_kwargs['con_pool_size'] = con_pool_size
                self._request = Request(**request_kwargs)
                self.bot = Bot(
                    token,  # type: ignore[arg-type]
                    base_url,
                    base_file_url=base_file_url,
                    request=self._request,
                    private_key=private_key,
                    private_key_password=private_key_password,
                    defaults=defaults,
                )
            self.update_queue: Queue = Queue()
            self.job_queue = JobQueue()
            self.__exception_event = Event()
            self.persistence = persistence
            self.dispatcher = Dispatcher(
                self.bot,
                self.update_queue,
                job_queue=self.job_queue,
                workers=workers,
                exception_event=self.__exception_event,
                persistence=persistence,
                use_context=use_context,
            )
            self.job_queue.set_dispatcher(self.dispatcher)
        else:
            con_pool_size = dispatcher.workers + 4

            self.bot = dispatcher.bot
            if self.bot.request.con_pool_size < con_pool_size:
                self.logger.warning(
                    'Connection pool of Request object is smaller than optimal value (%s)',
                    con_pool_size,
                )
            self.update_queue = dispatcher.update_queue
            self.__exception_event = dispatcher.exception_event
            self.persistence = dispatcher.persistence
            self.job_queue = dispatcher.job_queue
            self.dispatcher = dispatcher

        self.user_sig_handler = user_sig_handler
        self.last_update_id = 0
        self.running = False
        self.is_idle = False
        self.httpd = None
        self.__lock = Lock()
        self.__threads: List[Thread] = []

    def _init_thread(self, target: Callable, name: str, *args: Any, **kwargs: Any) -> None:
        thr = Thread(
            target=self._thread_wrapper,
            name="Bot:{}:{}".format(self.bot.id, name),
            args=(target,) + args,
            kwargs=kwargs,
        )
        thr.start()
        self.__threads.append(thr)

    def _thread_wrapper(self, target: Callable, *args: Any, **kwargs: Any) -> None:
        thr_name = current_thread().name
        self.logger.debug('%s - started', thr_name)
        try:
            target(*args, **kwargs)
        except Exception:
            self.__exception_event.set()
            self.logger.exception('unhandled exception in %s', thr_name)
            raise
        self.logger.debug('%s - ended', thr_name)

    def start_polling(
        self,
        poll_interval: float = 0.0,
        timeout: float = 10,
        clean: bool = False,
        bootstrap_retries: int = -1,
        read_latency: float = 2.0,
        allowed_updates: List[str] = None,
    ) -> Optional[Queue]:
        """Starts polling updates from Telegram.

        Args:
            poll_interval (:obj:`float`, optional): Time to wait between polling updates from
                Telegram in seconds. Default is 0.0.
            timeout (:obj:`float`, optional): Passed to :attr:`telegram.Bot.get_updates`.
            clean (:obj:`bool`, optional): Whether to clean any pending updates on Telegram servers
                before actually starting to poll. Default is :obj:`False`.
            bootstrap_retries (:obj:`int`, optional): Whether the bootstrapping phase of the
                `Updater` will retry on failures on the Telegram server.

                * < 0 - retry indefinitely (default)
                *   0 - no retries
                * > 0 - retry up to X times

            allowed_updates (List[:obj:`str`], optional): Passed to
                :attr:`telegram.Bot.get_updates`.
            read_latency (:obj:`float` | :obj:`int`, optional): Grace time in seconds for receiving
                the reply from server. Will be added to the `timeout` value and used as the read
                timeout from server (Default: 2).

        Returns:
            :obj:`Queue`: The update queue that can be filled from the main thread.

        """
        with self.__lock:
            if not self.running:
                self.running = True

                # Create & start threads
                self.job_queue.start()
                dispatcher_ready = Event()
                polling_ready = Event()
                self._init_thread(self.dispatcher.start, "dispatcher", ready=dispatcher_ready)
                self._init_thread(
                    self._start_polling,
                    "updater",
                    poll_interval,
                    timeout,
                    read_latency,
                    bootstrap_retries,
                    clean,
                    allowed_updates,
                    ready=polling_ready,
                )

                self.logger.debug('Waiting for Dispatcher and polling to start')
                dispatcher_ready.wait()
                polling_ready.wait()

                # Return the update queue so the main thread can insert updates
                return self.update_queue
            return None

    def start_webhook(
        self,
        listen: str = '127.0.0.1',
        port: int = 80,
        url_path: str = '',
        cert: str = None,
        key: str = None,
        clean: bool = False,
        bootstrap_retries: int = 0,
        webhook_url: str = None,
        allowed_updates: List[str] = None,
        force_event_loop: bool = False,
    ) -> Optional[Queue]:
        """
        Starts a small http server to listen for updates via webhook. If cert
        and key are not provided, the webhook will be started directly on
        http://listen:port/url_path, so SSL can be handled by another
        application. Else, the webhook will be started on
        https://listen:port/url_path

        Note:
            Due to an incompatibility of the Tornado library PTB uses for the webhook with Python
            3.8+ on Windows machines, PTB will attempt to set the event loop to
            :attr:`asyncio.SelectorEventLoop` and raise an exception, if an incompatible event loop
            has already been specified. See this `thread`_ for more details. To suppress the
            exception, set :attr:`force_event_loop` to :obj:`True`.

            .. _thread: https://github.com/tornadoweb/tornado/issues/2608

        Args:
            listen (:obj:`str`, optional): IP-Address to listen on. Default ``127.0.0.1``.
            port (:obj:`int`, optional): Port the bot should be listening on. Default ``80``.
            url_path (:obj:`str`, optional): Path inside url.
            cert (:obj:`str`, optional): Path to the SSL certificate file.
            key (:obj:`str`, optional): Path to the SSL key file.
            clean (:obj:`bool`, optional): Whether to clean any pending updates on Telegram servers
                before actually starting the webhook. Default is :obj:`False`.
            bootstrap_retries (:obj:`int`, optional): Whether the bootstrapping phase of the
                `Updater` will retry on failures on the Telegram server.

                * < 0 - retry indefinitely (default)
                *   0 - no retries
                * > 0 - retry up to X times

            webhook_url (:obj:`str`, optional): Explicitly specify the webhook url. Useful behind
                NAT, reverse proxy, etc. Default is derived from `listen`, `port` & `url_path`.
            allowed_updates (List[:obj:`str`], optional): Passed to
                :attr:`telegram.Bot.set_webhook`.
            force_event_loop (:obj:`bool`, optional): Force using the current event loop. See above
                note for details. Defaults to :obj:`False`

        Returns:
            :obj:`Queue`: The update queue that can be filled from the main thread.

        """
        with self.__lock:
            if not self.running:
                self.running = True

                # Create & start threads
                webhook_ready = Event()
                dispatcher_ready = Event()
                self.job_queue.start()
                self._init_thread(self.dispatcher.start, "dispatcher", dispatcher_ready)
                self._init_thread(
                    self._start_webhook,
                    "updater",
                    listen,
                    port,
                    url_path,
                    cert,
                    key,
                    bootstrap_retries,
                    clean,
                    webhook_url,
                    allowed_updates,
                    ready=webhook_ready,
                    force_event_loop=force_event_loop,
                )

                self.logger.debug('Waiting for Dispatcher and Webhook to start')
                webhook_ready.wait()
                dispatcher_ready.wait()

                # Return the update queue so the main thread can insert updates
                return self.update_queue
            return None

    @no_type_check
    def _start_polling(
        self,
        poll_interval,
        timeout,
        read_latency,
        bootstrap_retries,
        clean,
        allowed_updates,
        ready=None,
    ):  # pragma: no cover
        # Thread target of thread 'updater'. Runs in background, pulls
        # updates from Telegram and inserts them in the update queue of the
        # Dispatcher.

        self.logger.debug('Updater thread started (polling)')

        self._bootstrap(bootstrap_retries, clean=clean, webhook_url='', allowed_updates=None)

        self.logger.debug('Bootstrap done')

        def polling_action_cb():
            updates = self.bot.get_updates(
                self.last_update_id,
                timeout=timeout,
                read_latency=read_latency,
                allowed_updates=allowed_updates,
            )

            if updates:
                if not self.running:
                    self.logger.debug('Updates ignored and will be pulled again on restart')
                else:
                    for update in updates:
                        self.update_queue.put(update)
                    self.last_update_id = updates[-1].update_id + 1

            return True

        def polling_onerr_cb(exc):
            # Put the error into the update queue and let the Dispatcher
            # broadcast it
            self.update_queue.put(exc)

        if ready is not None:
            ready.set()

        self._network_loop_retry(
            polling_action_cb, polling_onerr_cb, 'getting Updates', poll_interval
        )

    @no_type_check
    def _network_loop_retry(self, action_cb, onerr_cb, description, interval):
        """Perform a loop calling `action_cb`, retrying after network errors.

        Stop condition for loop: `self.running` evaluates :obj:`False` or return value of
        `action_cb` evaluates :obj:`False`.

        Args:
            action_cb (:obj:`callable`): Network oriented callback function to call.
            onerr_cb (:obj:`callable`): Callback to call when TelegramError is caught. Receives the
                exception object as a parameter.
            description (:obj:`str`): Description text to use for logs and exception raised.
            interval (:obj:`float` | :obj:`int`): Interval to sleep between each call to
                `action_cb`.

        """
        self.logger.debug('Start network loop retry %s', description)
        cur_interval = interval
        while self.running:
            try:
                if not action_cb():
                    break
            except RetryAfter as exc:
                self.logger.info('%s', exc)
                cur_interval = 0.5 + exc.retry_after
            except TimedOut as toe:
                self.logger.debug('Timed out %s: %s', description, toe)
                # If failure is due to timeout, we should retry asap.
                cur_interval = 0
            except InvalidToken as pex:
                self.logger.error('Invalid token; aborting')
                raise pex
            except TelegramError as telegram_exc:
                self.logger.error('Error while %s: %s', description, telegram_exc)
                onerr_cb(telegram_exc)
                cur_interval = self._increase_poll_interval(cur_interval)
            else:
                cur_interval = interval

            if cur_interval:
                sleep(cur_interval)

    @staticmethod
    def _increase_poll_interval(current_interval: float) -> float:
        # increase waiting times on subsequent errors up to 30secs
        if current_interval == 0:
            current_interval = 1
        elif current_interval < 30:
            current_interval += current_interval / 2
        elif current_interval > 30:
            current_interval = 30
        return current_interval

    @no_type_check
    def _start_webhook(
        self,
        listen,
        port,
        url_path,
        cert,
        key,
        bootstrap_retries,
        clean,
        webhook_url,
        allowed_updates,
        ready=None,
        force_event_loop=False,
    ):
        self.logger.debug('Updater thread started (webhook)')
        use_ssl = cert is not None and key is not None
        if not url_path.startswith('/'):
            url_path = '/{}'.format(url_path)

        # Create Tornado app instance
        app = WebhookAppClass(url_path, self.bot, self.update_queue)

        # Form SSL Context
        # An SSLError is raised if the private key does not match with the certificate
        if use_ssl:
            try:
                ssl_ctx = ssl.create_default_context(ssl.Purpose.CLIENT_AUTH)
                ssl_ctx.load_cert_chain(cert, key)
            except ssl.SSLError as exc:
                raise TelegramError('Invalid SSL Certificate') from exc
        else:
            ssl_ctx = None

        # Create and start server
        self.httpd = WebhookServer(listen, port, app, ssl_ctx)

        if use_ssl:
            # DO NOT CHANGE: Only set webhook if SSL is handled by library
            if not webhook_url:
                webhook_url = self._gen_webhook_url(listen, port, url_path)

            self._bootstrap(
                max_retries=bootstrap_retries,
                clean=clean,
                webhook_url=webhook_url,
                cert=open(cert, 'rb'),
                allowed_updates=allowed_updates,
            )
        elif clean:
            self.logger.warning(
                "cleaning updates is not supported if "
                "SSL-termination happens elsewhere; skipping"
            )

        self.httpd.serve_forever(force_event_loop=force_event_loop, ready=ready)

    @staticmethod
    def _gen_webhook_url(listen: str, port: int, url_path: str) -> str:
        return 'https://{listen}:{port}{path}'.format(listen=listen, port=port, path=url_path)

    @no_type_check
    def _bootstrap(
        self, max_retries, clean, webhook_url, allowed_updates, cert=None, bootstrap_interval=5
    ):
        retries = [0]

        def bootstrap_del_webhook():
            self.bot.delete_webhook()
            return False

        def bootstrap_clean_updates():
            self.logger.debug('Cleaning updates from Telegram server')
            updates = self.bot.get_updates()
            while updates:
                updates = self.bot.get_updates(updates[-1].update_id + 1)
            return False

        def bootstrap_set_webhook():
            self.bot.set_webhook(
                url=webhook_url, certificate=cert, allowed_updates=allowed_updates
            )
            return False

        def bootstrap_onerr_cb(exc):
            if not isinstance(exc, Unauthorized) and (max_retries < 0 or retries[0] < max_retries):
                retries[0] += 1
                self.logger.warning(
                    'Failed bootstrap phase; try=%s max_retries=%s', retries[0], max_retries
                )
            else:
                self.logger.error('Failed bootstrap phase after %s retries (%s)', retries[0], exc)
                raise exc

        # Cleaning pending messages is done by polling for them - so we need to delete webhook if
        # one is configured.
        # We also take this chance to delete pre-configured webhook if this is a polling Updater.
        # NOTE: We don't know ahead if a webhook is configured, so we just delete.
        if clean or not webhook_url:
            self._network_loop_retry(
                bootstrap_del_webhook,
                bootstrap_onerr_cb,
                'bootstrap del webhook',
                bootstrap_interval,
            )
            retries[0] = 0

        # Clean pending messages, if requested.
        if clean:
            self._network_loop_retry(
                bootstrap_clean_updates,
                bootstrap_onerr_cb,
                'bootstrap clean updates',
                bootstrap_interval,
            )
            retries[0] = 0
            sleep(1)

        # Restore/set webhook settings, if needed. Again, we don't know ahead if a webhook is set,
        # so we set it anyhow.
        if webhook_url:
            self._network_loop_retry(
                bootstrap_set_webhook,
                bootstrap_onerr_cb,
                'bootstrap set webhook',
                bootstrap_interval,
            )

    def stop(self) -> None:
        """Stops the polling/webhook thread, the dispatcher and the job queue."""

        self.job_queue.stop()
        with self.__lock:
            if self.running or self.dispatcher.has_running_threads:
                self.logger.debug('Stopping Updater and Dispatcher...')

                self.running = False

                self._stop_httpd()
                self._stop_dispatcher()
                self._join_threads()

                # Stop the Request instance only if it was created by the Updater
                if self._request:
                    self._request.stop()

    @no_type_check
    def _stop_httpd(self) -> None:
        if self.httpd:
            self.logger.debug(
                'Waiting for current webhook connection to be '
                'closed... Send a Telegram message to the bot to exit '
                'immediately.'
            )
            self.httpd.shutdown()
            self.httpd = None

    @no_type_check
    def _stop_dispatcher(self) -> None:
        self.logger.debug('Requesting Dispatcher to stop...')
        self.dispatcher.stop()

    @no_type_check
    def _join_threads(self) -> None:
        for thr in self.__threads:
            self.logger.debug('Waiting for %s thread to end', thr.name)
            thr.join()
            self.logger.debug('%s thread has ended', thr.name)
        self.__threads = []

    @no_type_check
    def signal_handler(self, signum, frame) -> None:
        self.is_idle = False
        if self.running:
            self.logger.info(
                'Received signal %s (%s), stopping...', signum, get_signal_name(signum)
            )
            if self.persistence:
                # Update user_data, chat_data and bot_data before flushing
                self.dispatcher.update_persistence()
                self.persistence.flush()
            self.stop()
            if self.user_sig_handler:
                self.user_sig_handler(signum, frame)
        else:
            self.logger.warning('Exiting immediately!')
            # pylint: disable=C0415,W0212
            import os

            os._exit(1)

    def idle(self, stop_signals: Union[List, Tuple] = (SIGINT, SIGTERM, SIGABRT)) -> None:
        """Blocks until one of the signals are received and stops the updater.

        Args:
            stop_signals (:obj:`list` | :obj:`tuple`): List containing signals from the signal
                module that should be subscribed to. Updater.stop() will be called on receiving one
                of those signals. Defaults to (``SIGINT``, ``SIGTERM``, ``SIGABRT``).

        """
        for sig in stop_signals:
            signal(sig, self.signal_handler)

        self.is_idle = True

        while self.is_idle:
            sleep(1)
Exemplo n.º 15
0
def main():
    workers = 8
    con_pool_size = workers + 4

    job_queue = JobQueue()

    request = Request(con_pool_size=con_pool_size)
    bot = MQBot(token=TELEGRAM_API_TOKEN, request=request)
    dispatcher = Dispatcher(bot, Queue(),
                           job_queue=job_queue,
                           workers=workers)
    job_queue.set_dispatcher(dispatcher)
    updater = Updater(dispatcher=dispatcher, workers=None)

    xenian.bot.job_queue = job_queue

    def on_start():
        self = get_self(updater.bot)
        logger.info(f'Acting as {self.name} [link: {self.link}, id: {self.id}], with the key "{TELEGRAM_API_TOKEN}"')

    def stop_and_restart(chat_id):
        """Gracefully stop the Updater and replace the current process with a new one.
        """
        logger.info('Restarting: stopping')
        updater.stop()
        logger.info('Restarting: starting')
        os.execl(sys.executable, sys.executable, *sys.argv + [f'is_restart={chat_id}'])

    def restart(bot: Bot, update: Update):
        """Start the restarting process

        Args:
            bot (:obj:`telegram.bot.Bot`): Telegram Api Bot Object.
            update (:obj:`telegram.update.Update`): Telegram Api Update Object
        """
        update.message.reply_text('Bot is restarting...')
        Thread(target=lambda: stop_and_restart(update.message.chat_id)).start()

    def send_message_if_reboot():
        args = sys.argv
        is_restart_arg = [item for item in args if item.startswith('is_restart')]
        if any(is_restart_arg):
            chat_id = is_restart_arg[0].split('=')[1]
            updater.bot.send_message(chat_id, 'Bot has successfully restarted.')

    dispatcher.add_handler(CommandHandler('restart', restart, filters=Filters.user(username=ADMINS)))

    for command_class in BaseCommand.all_commands:
        for command in command_class.commands:
            dispatcher.add_handler(command['handler'](**command['options']), command['group'])

    # log all errors
    dispatcher.add_error_handler(error)

    if MODE['active'] == 'webhook':
        webhook = MODE['webhook']
        updater.start_webhook(listen=webhook['listen'], port=webhook['port'], url_path=webhook['url_path'])
        updater.bot.set_webhook(url=webhook['url'])
        send_message_if_reboot()
        logger.info('Starting webhook...')
        on_start()
    else:
        updater.start_polling()
        logger.info('Start polling...')
        send_message_if_reboot()
        on_start()
        updater.idle()
Exemplo n.º 16
0
class Bot(object):

    START_MESSAGE = "Hello, I am a bot, nice to meet you. You may use /help to read what my commands do."
    ADD_USAGE = "/add <url> <m> <keyword 1> <keyword 2> ..."
    LIST_USAGE = "/list"
    REMOVE_USAGE = "/remove <url>"

    # Help message.
    HELP_MESSAGE = f"""*KeywordScrapeBot*:\n
{ADD_USAGE}
Add a job that runs every m minutes (minimum 15), scanning the url for links containing the keywords.
Running the command again for the same url will overwrite the job.\n
{LIST_USAGE}
List your running jobs.\n
{REMOVE_USAGE}
Remove a job.
    """

    def __init__(self,
                 bot_token: str,
                 database_file: Union[str, Path],
                 minimum_interval: int = 15):
        """
        :param bot_token: The token to run the bot on.
        :param database_file: The database file.
        :param minimum_interval: The minimum update interval in minutes. Defaults to 15.
        """
        self._database_file = database_file
        self._minimum_interval = minimum_interval

        self._updater = Updater(token=bot_token, use_context=True)
        self._updater.dispatcher.add_handler(
            CommandHandler("start", self._add_user))
        self._updater.dispatcher.add_handler(
            CommandHandler("list", self._list_jobs))
        self._updater.dispatcher.add_handler(
            CommandHandler("add", self._add_job))
        self._updater.dispatcher.add_handler(
            CommandHandler("remove", self._remove_job))
        self._updater.dispatcher.add_handler(
            CommandHandler("help", self._send_help))

        self._job_queue = JobQueue()
        self._job_queue.set_dispatcher(self._updater.dispatcher)
        self._job_queue.start()

        # Keep map {(user, url) : telegram.ext.Job} to allow canceling jobs.
        self._job_map = dict()

        # Load all Jobs in the database.
        for job in Database(self._database_file).get_jobs():
            self._schedule(job)

    def start(self):
        self._updater.start_polling()

    def idle(self):
        self._updater.idle()

    def _add_user(self, update: telegram.Update,
                  context: telegram.ext.CallbackContext):
        """
        Callback for the addition of a user.
        """
        user = update.effective_chat.id
        # Add user to database.
        Database(self._database_file).add_user(user)
        # Answer user.
        context.bot.send_message(chat_id=user, text=self.START_MESSAGE)
        # Log the info about the new user.
        logging.info(f"/start command received by user: {user}.")

    def _add_job(self, update: telegram.Update,
                 context: telegram.ext.CallbackContext):
        f"""
        Callback for the update of a job. Message must be:
        ```
        {Bot.ADD_USAGE}
        ```
        """
        user = update.effective_chat.id
        try:
            # Extract info.
            url = context.args[0]

            # Check url validity.
            if not utils.is_valid_url(url):
                update.message.reply_text(f"{url} is not a valid url.",
                                          disable_web_page_preview=True)
                logging.warning(f"Invalid url from user {user}.")
                return

            # Check minimum time
            freq = int(context.args[1])
            if freq < self._minimum_interval:
                update.message.reply_text(
                    f"{self._minimum_interval} minutes is the minimum time. I'll just set it for you."
                )
                freq = self._minimum_interval

            keywords = context.args[2::] if len(context.args) > 2 else list()

            # Update database.
            job = Job(user, url, freq, keywords)
            Database(self._database_file).add_job(job)

            # Schedule job.
            self._schedule(job)

            # Send back a response as a confirmation.
            response = f"Will start searching {url} for links containing {', '.join(keywords)} every {freq} minutes."
            update.message.reply_text(response, disable_web_page_preview=True)
            logging.info(f"/add command received by user: {user}. {response}")

        except (IndexError, ValueError):
            update.message.reply_text(f"Usage: {Bot.ADD_USAGE}")
            logging.warning(f"Inappropriate /add command from user {user}.")

    def _list_jobs(self, update: telegram.Update,
                   context: telegram.ext.CallbackContext):
        """
        Send a message containing the scheduled jobs for the user.
        """
        user = update.effective_chat.id
        jobs = Database(self._database_file).get_jobs(user)
        if jobs:
            update.message.reply_markdown("\n---\n".join([
                f"*JOB {i + 1}*\nurl: {j.url}\nkeywords: {j.keywords}\nEvery {j.freq} hours."
                for i, j in enumerate(jobs)
            ]),
                                          disable_web_page_preview=True)
        else:
            update.message.reply_text(f"No jobs scheduled.")
        logging.info(f"Sent job list to {user}.")

    def _remove_job(self, update: telegram.Update,
                    context: telegram.ext.CallbackContext):
        f"""
        Callback for the removal of a job. Message must be:
        ```
        {Bot.REMOVE_USAGE}
        ```
        """
        user = update.effective_chat.id
        try:
            url = context.args[0]
            db = Database(self._database_file)
            jobs = db.get_jobs(user)

            # Job not in database.
            if url not in [j.url for j in jobs]:
                update.message.reply_text(f"You have no job for url: {url}",
                                          disable_web_page_preview=True)
                logging.info(
                    f"User {user} asked for removal of non-existing job {url}")
                return

            # Job in db, delete and unschedule job.
            db.delete_job(user, url)
            self._unschedule(user, url)

            # Send back a response.
            update.message.reply_text(
                f"You will receive no more updates from: {url}",
                disable_web_page_preview=True)
            logging.info(f"Removed job {url} for user {user}.")

        except IndexError:
            update.message.reply_text(f"Usage: {Bot.REMOVE_USAGE}")
            logging.warning(f"Inappropriate /remove command from user {user}.")

    def _send_help(self, update: telegram.Update,
                   context: telegram.ext.CallbackContext):
        """
        Send help message.
        """
        user = update.effective_chat.id
        # Answer user.
        context.bot.send_message(chat_id=user,
                                 text=self.HELP_MESSAGE,
                                 parse_mode="markdown")
        logging.info(f"Sent help to user: {user}.")

    def _schedule(self, job: Job):
        """
        Schedule a new job for the bot. Tries to remove any previous job for the same key (user, url)

        :param job: The new job to schedule.
        """
        # Safely remove any old matching job.
        self._unschedule(job.user, job.url)

        # Set job to run every x hours and keep track to cancel it later.
        self._job_map[(job.user, job.url)] = self._job_queue.run_repeating(
            make_job_callback(job, self._database_file), 60 * job.freq, 1)
        logging.info(f"Started job on url {job.url} for user {job.user}.")

    def _unschedule(self, user: int, url: str):
        """
        Remove the corresponding job from the queue.

        :param user: The user of the job.
        :param url: The url of the job.
        """
        old_job = self._job_map.pop((user, url), 0)
        if old_job != 0:
            old_job.schedule_removal()
            logging.info(f"Removed Telegram job on url {url} for user {user}.")

    def __del__(self):
        # Stop za bot.
        self._job_queue.stop()
        self._updater.stop()
Exemplo n.º 17
0
class Updater(object):
    """
    This class, which employs the :class:`telegram.ext.Dispatcher`, provides a frontend to
    :class:`telegram.Bot` to the programmer, so they can focus on coding the bot. Its purpose is to
    receive the updates from Telegram and to deliver them to said dispatcher. It also runs in a
    separate thread, so the user can interact with the bot, for example on the command line. The
    dispatcher supports handlers for different kinds of data: Updates from Telegram, basic text
    commands and even arbitrary types. The updater can be started as a polling service or, for
    production, use a webhook to receive updates. This is achieved using the WebhookServer and
    WebhookHandler classes.


    Attributes:
        bot (:class:`telegram.Bot`): The bot used with this Updater.
        user_sig_handler (:obj:`signal`): signals the updater will respond to.
        update_queue (:obj:`Queue`): Queue for the updates.
        job_queue (:class:`telegram.ext.JobQueue`): Jobqueue for the updater.
        dispatcher (:class:`telegram.ext.Dispatcher`): Dispatcher that handles the updates and
            dispatches them to the handlers.
        running (:obj:`bool`): Indicates if the updater is running.
        use_context (:obj:`bool`, optional): ``True`` if using context based callbacks.

    Args:
        token (:obj:`str`, optional): The bot's token given by the @BotFather.
        base_url (:obj:`str`, optional): Base_url for the bot.
        workers (:obj:`int`, optional): Amount of threads in the thread pool for functions
            decorated with ``@run_async``.
        bot (:class:`telegram.Bot`, optional): A pre-initialized bot instance. If a pre-initialized
            bot is used, it is the user's responsibility to create it using a `Request`
            instance with a large enough connection pool.
        user_sig_handler (:obj:`function`, optional): Takes ``signum, frame`` as positional
            arguments. This will be called when a signal is received, defaults are (SIGINT,
            SIGTERM, SIGABRT) setable with :attr:`idle`.
        request_kwargs (:obj:`dict`, optional): Keyword args to control the creation of a
            `telegram.utils.request.Request` object (ignored if `bot` argument is used). The
            request_kwargs are very useful for the advanced users who would like to control the
            default timeouts and/or control the proxy used for http communication.
        use_context (:obj:`bool`, optional): If set to ``True`` Use the context based callback API.
            During the deprecation period of the old API the default is ``False``. **New users**:
            set this to ``True``.

    Note:
        You must supply either a :attr:`bot` or a :attr:`token` argument.

    Raises:
        ValueError: If both :attr:`token` and :attr:`bot` are passed or none of them.

    """

    _request = None

    def __init__(self,
                 token=None,
                 base_url=None,
                 workers=4,
                 bot=None,
                 user_sig_handler=None,
                 request_kwargs=None,
                 use_context=False):

        if (token is None) and (bot is None):
            raise ValueError('`token` or `bot` must be passed')
        if (token is not None) and (bot is not None):
            raise ValueError('`token` and `bot` are mutually exclusive')

        self.logger = logging.getLogger(__name__)

        con_pool_size = workers + 4

        if bot is not None:
            self.bot = bot
            if bot.request.con_pool_size < con_pool_size:
                self.logger.warning(
                    'Connection pool of Request object is smaller than optimal value (%s)',
                    con_pool_size)
        else:
            # we need a connection pool the size of:
            # * for each of the workers
            # * 1 for Dispatcher
            # * 1 for polling Updater (even if webhook is used, we can spare a connection)
            # * 1 for JobQueue
            # * 1 for main thread
            if request_kwargs is None:
                request_kwargs = {}
            if 'con_pool_size' not in request_kwargs:
                request_kwargs['con_pool_size'] = con_pool_size
            self._request = Request(**request_kwargs)
            self.bot = Bot(token, base_url, request=self._request)
        self.user_sig_handler = user_sig_handler
        self.update_queue = Queue()
        self.job_queue = JobQueue()
        self.__exception_event = Event()
        self.dispatcher = Dispatcher(self.bot,
                                     self.update_queue,
                                     job_queue=self.job_queue,
                                     workers=workers,
                                     exception_event=self.__exception_event,
                                     use_context=use_context)
        self.job_queue.set_dispatcher(self.dispatcher)
        self.last_update_id = 0
        self.running = False
        self.is_idle = False
        self.httpd = None
        self.__lock = Lock()
        self.__threads = []

    def _init_thread(self, target, name, *args, **kwargs):
        thr = Thread(target=self._thread_wrapper,
                     name=name,
                     args=(target, ) + args,
                     kwargs=kwargs)
        thr.start()
        self.__threads.append(thr)

    def _thread_wrapper(self, target, *args, **kwargs):
        thr_name = current_thread().name
        self.logger.debug('{0} - started'.format(thr_name))
        try:
            target(*args, **kwargs)
        except Exception:
            self.__exception_event.set()
            self.logger.exception('unhandled exception in %s', thr_name)
            raise
        self.logger.debug('{0} - ended'.format(thr_name))

    def start_polling(self,
                      poll_interval=0.0,
                      timeout=10,
                      clean=False,
                      bootstrap_retries=-1,
                      read_latency=2.,
                      allowed_updates=None):
        """Starts polling updates from Telegram.

        Args:
            poll_interval (:obj:`float`, optional): Time to wait between polling updates from
                Telegram in seconds. Default is 0.0.
            timeout (:obj:`float`, optional): Passed to :attr:`telegram.Bot.get_updates`.
            clean (:obj:`bool`, optional): Whether to clean any pending updates on Telegram servers
                before actually starting to poll. Default is False.
            bootstrap_retries (:obj:`int`, optional): Whether the bootstrapping phase of the
                `Updater` will retry on failures on the Telegram server.

                * < 0 - retry indefinitely (default)
                *   0 - no retries
                * > 0 - retry up to X times

            allowed_updates (List[:obj:`str`], optional): Passed to
                :attr:`telegram.Bot.get_updates`.
            read_latency (:obj:`float` | :obj:`int`, optional): Grace time in seconds for receiving
                the reply from server. Will be added to the `timeout` value and used as the read
                timeout from server (Default: 2).

        Returns:
            :obj:`Queue`: The update queue that can be filled from the main thread.

        """
        with self.__lock:
            if not self.running:
                self.running = True

                # Create & start threads
                self.job_queue.start()
                dispatcher_ready = Event()
                self._init_thread(self.dispatcher.start,
                                  "dispatcher",
                                  ready=dispatcher_ready)
                self._init_thread(self._start_polling, "updater",
                                  poll_interval, timeout, read_latency,
                                  bootstrap_retries, clean, allowed_updates)

                dispatcher_ready.wait()

                # Return the update queue so the main thread can insert updates
                return self.update_queue

    def start_webhook(self,
                      listen='127.0.0.1',
                      port=80,
                      url_path='',
                      cert=None,
                      key=None,
                      clean=False,
                      bootstrap_retries=0,
                      webhook_url=None,
                      allowed_updates=None):
        """
        Starts a small http server to listen for updates via webhook. If cert
        and key are not provided, the webhook will be started directly on
        http://listen:port/url_path, so SSL can be handled by another
        application. Else, the webhook will be started on
        https://listen:port/url_path

        Args:
            listen (:obj:`str`, optional): IP-Address to listen on. Default ``127.0.0.1``.
            port (:obj:`int`, optional): Port the bot should be listening on. Default ``80``.
            url_path (:obj:`str`, optional): Path inside url.
            cert (:obj:`str`, optional): Path to the SSL certificate file.
            key (:obj:`str`, optional): Path to the SSL key file.
            clean (:obj:`bool`, optional): Whether to clean any pending updates on Telegram servers
                before actually starting the webhook. Default is ``False``.
            bootstrap_retries (:obj:`int`, optional): Whether the bootstrapping phase of the
                `Updater` will retry on failures on the Telegram server.

                * < 0 - retry indefinitely (default)
                *   0 - no retries
                * > 0 - retry up to X times

            webhook_url (:obj:`str`, optional): Explicitly specify the webhook url. Useful behind
                NAT, reverse proxy, etc. Default is derived from `listen`, `port` & `url_path`.
            allowed_updates (List[:obj:`str`], optional): Passed to
                :attr:`telegram.Bot.set_webhook`.

        Returns:
            :obj:`Queue`: The update queue that can be filled from the main thread.

        """
        with self.__lock:
            if not self.running:
                self.running = True

                # Create & start threads
                self.job_queue.start()
                self._init_thread(self.dispatcher.start, "dispatcher"),
                self._init_thread(self._start_webhook, "updater", listen, port,
                                  url_path, cert, key, bootstrap_retries,
                                  clean, webhook_url, allowed_updates)

                # Return the update queue so the main thread can insert updates
                return self.update_queue

    def _start_polling(self, poll_interval, timeout, read_latency,
                       bootstrap_retries, clean,
                       allowed_updates):  # pragma: no cover
        # Thread target of thread 'updater'. Runs in background, pulls
        # updates from Telegram and inserts them in the update queue of the
        # Dispatcher.

        self.logger.debug('Updater thread started (polling)')

        self._bootstrap(bootstrap_retries,
                        clean=clean,
                        webhook_url='',
                        allowed_updates=None)

        self.logger.debug('Bootstrap done')

        def polling_action_cb():
            updates = self.bot.get_updates(self.last_update_id,
                                           timeout=timeout,
                                           read_latency=read_latency,
                                           allowed_updates=allowed_updates)

            if updates:
                if not self.running:
                    self.logger.debug(
                        'Updates ignored and will be pulled again on restart')
                else:
                    for update in updates:
                        self.update_queue.put(update)
                    self.last_update_id = updates[-1].update_id + 1

            return True

        def polling_onerr_cb(exc):
            # Put the error into the update queue and let the Dispatcher
            # broadcast it
            self.update_queue.put(exc)

        self._network_loop_retry(polling_action_cb, polling_onerr_cb,
                                 'getting Updates', poll_interval)

    def _network_loop_retry(self, action_cb, onerr_cb, description, interval):
        """Perform a loop calling `action_cb`, retrying after network errors.

        Stop condition for loop: `self.running` evaluates False or return value of `action_cb`
        evaluates False.

        Args:
            action_cb (:obj:`callable`): Network oriented callback function to call.
            onerr_cb (:obj:`callable`): Callback to call when TelegramError is caught. Receives the
                exception object as a parameter.
            description (:obj:`str`): Description text to use for logs and exception raised.
            interval (:obj:`float` | :obj:`int`): Interval to sleep between each call to
                `action_cb`.

        """
        self.logger.debug('Start network loop retry %s', description)
        cur_interval = interval
        while self.running:
            try:
                if not action_cb():
                    break
            except RetryAfter as e:
                self.logger.info('%s', e)
                cur_interval = 0.5 + e.retry_after
            except TimedOut as toe:
                self.logger.debug('Timed out %s: %s', description, toe)
                # If failure is due to timeout, we should retry asap.
                cur_interval = 0
            except InvalidToken as pex:
                self.logger.error('Invalid token; aborting')
                raise pex
            except TelegramError as te:
                self.logger.error('Error while %s: %s', description, te)
                onerr_cb(te)
                cur_interval = self._increase_poll_interval(cur_interval)
            else:
                cur_interval = interval

            if cur_interval:
                sleep(cur_interval)

    @staticmethod
    def _increase_poll_interval(current_interval):
        # increase waiting times on subsequent errors up to 30secs
        if current_interval == 0:
            current_interval = 1
        elif current_interval < 30:
            current_interval += current_interval / 2
        elif current_interval > 30:
            current_interval = 30
        return current_interval

    def _start_webhook(self, listen, port, url_path, cert, key,
                       bootstrap_retries, clean, webhook_url, allowed_updates):
        self.logger.debug('Updater thread started (webhook)')
        use_ssl = cert is not None and key is not None
        if not url_path.startswith('/'):
            url_path = '/{0}'.format(url_path)

        # Create and start server
        self.httpd = WebhookServer((listen, port), WebhookHandler,
                                   self.update_queue, url_path, self.bot)

        if use_ssl:
            self._check_ssl_cert(cert, key)

            # DO NOT CHANGE: Only set webhook if SSL is handled by library
            if not webhook_url:
                webhook_url = self._gen_webhook_url(listen, port, url_path)

            self._bootstrap(max_retries=bootstrap_retries,
                            clean=clean,
                            webhook_url=webhook_url,
                            cert=open(cert, 'rb'),
                            allowed_updates=allowed_updates)
        elif clean:
            self.logger.warning("cleaning updates is not supported if "
                                "SSL-termination happens elsewhere; skipping")

        self.httpd.serve_forever(poll_interval=1)

    def _check_ssl_cert(self, cert, key):
        # Check SSL-Certificate with openssl, if possible
        try:
            exit_code = subprocess.call(
                ["openssl", "x509", "-text", "-noout", "-in", cert],
                stdout=open(os.devnull, 'wb'),
                stderr=subprocess.STDOUT)
        except OSError:
            exit_code = 0
        if exit_code is 0:
            try:
                self.httpd.socket = ssl.wrap_socket(self.httpd.socket,
                                                    certfile=cert,
                                                    keyfile=key,
                                                    server_side=True)
            except ssl.SSLError as error:
                self.logger.exception('Failed to init SSL socket')
                raise TelegramError(str(error))
        else:
            raise TelegramError('SSL Certificate invalid')

    @staticmethod
    def _gen_webhook_url(listen, port, url_path):
        return 'https://{listen}:{port}{path}'.format(listen=listen,
                                                      port=port,
                                                      path=url_path)

    def _bootstrap(self,
                   max_retries,
                   clean,
                   webhook_url,
                   allowed_updates,
                   cert=None,
                   bootstrap_interval=5):
        retries = [0]

        def bootstrap_del_webhook():
            self.bot.delete_webhook()
            return False

        def bootstrap_clean_updates():
            self.logger.debug('Cleaning updates from Telegram server')
            updates = self.bot.get_updates()
            while updates:
                updates = self.bot.get_updates(updates[-1].update_id + 1)
            return False

        def bootstrap_set_webhook():
            self.bot.set_webhook(url=webhook_url,
                                 certificate=cert,
                                 allowed_updates=allowed_updates)
            return False

        def bootstrap_onerr_cb(exc):
            if not isinstance(exc, Unauthorized) and (
                    max_retries < 0 or retries[0] < max_retries):
                retries[0] += 1
                self.logger.warning(
                    'Failed bootstrap phase; try=%s max_retries=%s',
                    retries[0], max_retries)
            else:
                self.logger.error(
                    'Failed bootstrap phase after %s retries (%s)', retries[0],
                    exc)
                raise exc

        # Cleaning pending messages is done by polling for them - so we need to delete webhook if
        # one is configured.
        # We also take this chance to delete pre-configured webhook if this is a polling Updater.
        # NOTE: We don't know ahead if a webhook is configured, so we just delete.
        if clean or not webhook_url:
            self._network_loop_retry(bootstrap_del_webhook, bootstrap_onerr_cb,
                                     'bootstrap del webhook',
                                     bootstrap_interval)
            retries[0] = 0

        # Clean pending messages, if requested.
        if clean:
            self._network_loop_retry(bootstrap_clean_updates,
                                     bootstrap_onerr_cb,
                                     'bootstrap clean updates',
                                     bootstrap_interval)
            retries[0] = 0
            sleep(1)

        # Restore/set webhook settings, if needed. Again, we don't know ahead if a webhook is set,
        # so we set it anyhow.
        if webhook_url:
            self._network_loop_retry(bootstrap_set_webhook, bootstrap_onerr_cb,
                                     'bootstrap set webhook',
                                     bootstrap_interval)

    def stop(self):
        """Stops the polling/webhook thread, the dispatcher and the job queue."""

        self.job_queue.stop()
        with self.__lock:
            if self.running or self.dispatcher.has_running_threads:
                self.logger.debug('Stopping Updater and Dispatcher...')

                self.running = False

                self._stop_httpd()
                self._stop_dispatcher()
                self._join_threads()

                # Stop the Request instance only if it was created by the Updater
                if self._request:
                    self._request.stop()

    def _stop_httpd(self):
        if self.httpd:
            self.logger.debug(
                'Waiting for current webhook connection to be '
                'closed... Send a Telegram message to the bot to exit '
                'immediately.')
            self.httpd.shutdown()
            self.httpd = None

    def _stop_dispatcher(self):
        self.logger.debug('Requesting Dispatcher to stop...')
        self.dispatcher.stop()

    def _join_threads(self):
        for thr in self.__threads:
            self.logger.debug('Waiting for {0} thread to end'.format(thr.name))
            thr.join()
            self.logger.debug('{0} thread has ended'.format(thr.name))
        self.__threads = []

    def signal_handler(self, signum, frame):
        self.is_idle = False
        if self.running:
            self.logger.info('Received signal {} ({}), stopping...'.format(
                signum, get_signal_name(signum)))
            self.stop()
            if self.user_sig_handler:
                self.user_sig_handler(signum, frame)
        else:
            self.logger.warning('Exiting immediately!')
            import os
            os._exit(1)

    def idle(self, stop_signals=(SIGINT, SIGTERM, SIGABRT)):
        """Blocks until one of the signals are received and stops the updater.

        Args:
            stop_signals (:obj:`iterable`): Iterable containing signals from the signal module that
                should be subscribed to. Updater.stop() will be called on receiving one of those
                signals. Defaults to (``SIGINT``, ``SIGTERM``, ``SIGABRT``).

        """
        for sig in stop_signals:
            signal(sig, self.signal_handler)

        self.is_idle = True

        while self.is_idle:
            sleep(1)
Exemplo n.º 18
0
class Bot:
    def __init__(self, logger, postgres):
        self._status = {}
        self._logger = logger
        self._postgres = postgres
        # self._updater = Updater(token=BOT_TOKEN, use_context=True)
        self._bot = TelegramBot(token=BOT_TOKEN)
        self._job_queue = JobQueue()
        self._update_queue = Queue()
        self._dispatcher = Dispatcher(self._bot, self._update_queue, use_context=True)
        self._translator = Translator(file=TRANSLATION_FILE)
        self._set_commands()
        self._load_status()
        self._set_job_queue()

# ------------------------------------------------------------------------------------------
#       INIT METHODS
# ------------------------------------------------------------------------------------------

    def _set_commands(self):
        self._dispatcher.add_handler(CommandHandler(START, self._start))
        self._dispatcher.add_handler(CommandHandler(ADD, self._add))
        self._dispatcher.add_handler(CommandHandler(FRIENDS, self._friends))
        self._dispatcher.add_handler(CommandHandler(LANGUAGE, self._language))
        self._dispatcher.add_handler(CommandHandler(HELP, self._help))
        self._dispatcher.add_handler(CallbackQueryHandler(self._callback_query))
        self._dispatcher.add_handler(MessageHandler(Filters.all, self._other_messages))
        # self._updater.dispatcher.add_handler(CommandHandler(START, self._start))
        # self._updater.dispatcher.add_handler(CommandHandler(ADD, self._add))
        # self._updater.dispatcher.add_handler(CommandHandler(FRIENDS, self._friends))
        # self._updater.dispatcher.add_handler(CommandHandler(LANGUAGE, self._language))
        # self._updater.dispatcher.add_handler(CommandHandler(HELP, self._help))
        # self._updater.dispatcher.add_handler(CallbackQueryHandler(self._callback_query))
        # self._updater.dispatcher.add_handler(MessageHandler(Filters.all, self._other_messages))

    def _load_status(self):
        command = self._postgres.commands().select_account()
        records = self._postgres.execute(command)
        if self._correct_postgres_answer(records):
            for record in records:
                self._status[record[0]] = json.loads(record[5])

    def _set_job_queue(self):
        self._job_queue.set_dispatcher(self._dispatcher)
        now = datetime.utcnow()
        to = now + timedelta(seconds=24 * 60 * 60)
        to = to.replace(hour=0, minute=0, second=30, microsecond=0)
        self._job_queue.run_repeating(
            self._it_is_time_for_birthday,
            interval=24 * 60 * 60,
            first=to.timestamp() - now.timestamp()
        )
        self._job_queue.start()

# ------------------------------------------------------------------------------------------
#       PUBLIC METHODS
# ------------------------------------------------------------------------------------------

    def start_pooling(self):
        # self._updater.start_polling()
        # self._updater.idle()
        self._bot.setWebhook(HEROKU_APP_URL + BOT_TOKEN)

    def get_dispatcher(self):
        # pass
        return self._dispatcher

    def get_update_queue(self):
        # pass
        return self._update_queue

    def get_bot(self):
        # pass
        return self._bot

# ------------------------------------------------------------------------------------------
#       COMMANDS
# ------------------------------------------------------------------------------------------

    def _start(self, update, context):
        account_id = update.effective_user[ACCOUNT_ID]
        language = self._status[account_id][LANGUAGE] if self._status.get(account_id, None) else STANDARD_LANGUAGE
        translate = self._translator.translate
        if account_id in self._status.keys():
            self._status[account_id][STATUS] = NONE
            context.bot.send_message(
                chat_id=update.effective_message.chat_id,
                text=translate("Я рад снова тебя приветствовать здесь!", language)
            )
        else:
            self._status[account_id] = {
                LANGUAGE: STANDARD_LANGUAGE,
                STATUS: NONE,
                BIRTHDAY: {}
            }
            command = self._postgres.commands().insert_account(
                account_id=account_id,
                first_name=update.effective_user[FIRST_NAME],
                last_name=update.effective_user[LAST_NAME],
                user_name=update.effective_user[USERNAME],
                language_code=STANDARD_LANGUAGE,
                status=json.dumps(self._status[account_id])
            )
            self._postgres.execute(command)
            context.bot.send_message(
                chat_id=update.effective_message.chat_id,
                text=translate("Я рад тебя приветствовать у меня в гостях. У меня уютно и есть печеньки!", language)
            )
            context.bot.send_message(
                chat_id=update.effective_message.chat_id,
                text=translate("Чтобы вы лучше понимали что я могу, воспользуйтесь командой /help. Если у вас есть желание сменить язык общения, то команда /language поможет вам это сделать!", language)
            )
        self._update_status(account_id)

    def _add(self, update, context):
        account_id = update.effective_user[ACCOUNT_ID]
        language = self._status[account_id][LANGUAGE]
        translate = self._translator.translate
        self._status[account_id][STATUS] = ADD_CONTACT
        self._status[account_id][BIRTHDAY] = {
            FIO: {
                LAST_NAME: NONE,
                FIRST_NAME: NONE,
                MIDDLE_NAME: NONE
            },
            DATE: {
                YEAR: NONE,
                MONTH: NONE,
                DAY: NONE
            },
            CONGRATULATION: NONE,
            DESIRES: NONE,
            PHONE_NUMBER: NONE,
            TELEGRAM_USER_ID: NONE
        }

        context.bot.send_message(
            chat_id=update.effective_message.chat_id,
            text=translate("Давайте начнем заполнение анкеты вашего друга. Сперва пришлите контакт друга", language)
        )
        self._update_status(account_id)

    def _friends(self, update, context):
        account_id = update.effective_user[ACCOUNT_ID]
        language = self._status[account_id][LANGUAGE]
        translate = self._translator.translate
        self._status[account_id][STATUS] = NONE

        birthday_command = self._postgres.commands().select_birthday_for_account(account_id)
        birthday_records = self._postgres.execute(birthday_command)
        if self._correct_postgres_answer(birthday_records):
            text = translate("Вот список добавленных друзей:", language) + "\n\n"
            for birthday_record in birthday_records:
                text += "{fio} {birthday}\n".format(
                    fio=str(" ".join([birthday_record[3], birthday_record[1], birthday_record[2]])).strip(),
                    birthday=birthday_record[6].strftime("%Y-%m-%d")
                )
            context.bot.send_message(
                chat_id=update.message.chat_id,
                text=text[:-1]
            )
        else:
            context.bot.send_message(
                chat_id=update.message.chat_id,
                text=translate("Вы еще не добавили ни одного друга!", language)
            )
        self._update_status(account_id)

    def _language(self, update, context):
        account_id = update.effective_user[ACCOUNT_ID]
        language = self._status[account_id][LANGUAGE]
        translate = self._translator.translate
        self._status[account_id][STATUS] = LANGUAGE
        languages = [(key, value) for key, value in self._translator.languages().items() if key != language]
        keyboard = [
            [
                InlineKeyboardButton(languages[0][1], callback_data=languages[0][0]),
                InlineKeyboardButton(languages[1][1], callback_data=languages[1][0])
            ]
        ]
        reply_markup = InlineKeyboardMarkup(keyboard)
        context.bot.send_message(
            chat_id=update.effective_message.chat_id,
            text=translate("Сейчас установленный язык русский. На какой Вы желаете изменить?", language),
            reply_markup=reply_markup
        )
        self._update_status(account_id)

    def _help(self, update, context):
        account_id = update.effective_user[ACCOUNT_ID]
        language = self._status[account_id][LANGUAGE]
        translate = self._translator.translate
        self._status[account_id][STATUS] = NONE
        context.bot.send_message(
            chat_id=update.message.chat_id,
            text=translate("BirthdayBot создан для напоминания о днях рождениях твоих друзей! "
                           "Вдруг у вас много работы или дел по дому, то я всегда дам вам знать, "
                           "что особенный день близко!\\n\\n"
                           "Чтобы я смог напомнить вам о дне рождения, вам необходимо заполнить анкету друга! "
                           "Для заполнения анкеты существует команда /add\\n\\n"
                           "При необходимости сменить язык общения - можно отправить команду /language", language)
        )
        self._update_status(account_id)

    def _callback_query(self, update, context):
        account_id = update.effective_user[ACCOUNT_ID]
        query_data = update.callback_query.data
        status = self._status[account_id][STATUS]
        if status == LANGUAGE:
            self._language_in_callback_query(update, context)
        elif status == CREATE:
            context.bot.answer_callback_query(callback_query_id=update.callback_query.id)
            if query_data == ADD_FIO:
                self._add_fio_in_callback_query(update, context)
            elif query_data == ADD_DATE:
                self._add_date_in_callback_query(update, context)
            elif query_data == ADD_CONGRATULATION:
                self._add_congratulation_in_callback_query(update, context)
            elif query_data == ADD_DESIRES:
                self._add_desires_in_callback_query(update, context)
            elif query_data == CREATE:
                self._create_in_callback_query(update, context)
        else:
            self._invalid_in_callback_query(update, context)

    def _other_messages(self, update, context):
        account_id = update.effective_user[ACCOUNT_ID]
        status = self._status[account_id][STATUS]

        if status == ADD_CONTACT:
            self._add_contact_in_other_messages(update, context)
        elif status == ADD_DATE_INTERVAL:
            self._add_date_interval_in_other_messages(update, context)
        elif status == ADD_DATE_YEAR:
            self._add_date_year_in_other_messages(update, context)
        elif status == ADD_DATE_MONTH:
            self._add_date_month_in_other_messages(update, context)
        elif status == ADD_DATE_DAY:
            self._add_date_day_in_other_messages(update, context)
        elif status == ADD_FIO_LAST_NAME:
            self._add_fio_last_name_in_other_messages(update, context)
        elif status == ADD_FIO_FIRST_NAME:
            self._add_fio_first_name_in_other_messages(update, context)
        elif status == ADD_FIO_MIDDLE_NAME:
            self._add_fio_middle_name_in_other_messages(update, context)
        elif status == ADD_CONGRATULATION:
            self._add_congratulation_in_other_messages(update, context)
        elif status == ADD_DESIRES:
            self._add_desires_in_other_messages(update, context)
        else:
            self._invalid_in_other_messages(update, context)

# ------------------------------------------------------------------------------------------
#       JOBQUEUE METHODS
# ------------------------------------------------------------------------------------------

    def _it_is_time_for_birthday(self, dispatcher):
        self._logger.info("_it_is_time_for_birthday", "i am here")

        account_command = self._postgres.commands().select_account()
        account_records = self._postgres.execute(account_command)
        translate = self._translator.translate
        if self._correct_postgres_answer(account_records):
            for account_record in account_records:
                account_id = account_record[0]
                language = self._status[account_id][LANGUAGE]

                birthday_command = self._postgres.commands().select_birthday_for_account(account_record[0])
                birthday_records = self._postgres.execute(birthday_command)
                if self._correct_postgres_answer(birthday_records):
                    for birthday_record in birthday_records:
                        datetime_birthday = datetime.strptime(birthday_record[6].strftime("%Y-%m-%d"), "%Y-%m-%d")

                        birthday = {
                            FIO: {
                                LAST_NAME: birthday_record[1],
                                FIRST_NAME: birthday_record[2],
                                MIDDLE_NAME: birthday_record[3]
                            },
                            DATE: {
                                YEAR: str(datetime_birthday.year),
                                MONTH: str(datetime_birthday.month),
                                DAY: str(datetime_birthday.day)
                            }
                        }
                        remind7, remind1 = birthday_record[9], birthday_record[10]
                        datetime_birthday = datetime_birthday.replace(year=datetime.utcnow().year)
                        if datetime.utcnow().timestamp() > datetime_birthday.timestamp():
                            datetime_birthday = datetime_birthday.replace(year=datetime.utcnow().year + 1)
                        if datetime_birthday.timestamp() - datetime.utcnow().timestamp() <= 24 * 60 * 60 and remind1:
                            dispatcher.bot.send_message(
                                chat_id=account_record[0],
                                text=translate("У твоего друга менее чем через сутки день рождения!\\n\\n"
                                               "{fio} исполняется {age}!\\n\\n"
                                               "Не забудь поздравить именинника и постарайся сделать его день рождения незабываемым! "
                                               "Надеюсь, что подарок ты уже приготовил!", language).format(
                                    fio=str(" ".join(birthday[FIO].values())).strip(),
                                    age=str(datetime.utcnow().year - int(birthday[DATE][YEAR])),
                                )
                            )
                            remind7, remind1 = True, True
                        elif datetime_birthday.timestamp() - datetime.utcnow().timestamp() <= 7 * 24 * 60 * 60 and remind7:
                            dispatcher.bot.send_message(
                                chat_id=account_record[0],
                                text=translate(
                                    "У твоего друга менее чем через неделю день рождения!\\n\\n"
                                    "{fio} исполнится {age}!\\n\\n" 
                                    "Приготовь хороший подарок, надеюсь ты знаешь что бы он хотел! "
                                    "Не забудь поздравить именинника и постарайся "
                                    "сделать его день рождения незабываемым!", language).format(
                                    fio=str(" ".join(birthday[FIO].values())).strip(),
                                    age=str(datetime.utcnow().year - int(birthday[DATE][YEAR])),
                                )
                            )
                            remind7, remind1 = False, True
                        command = self._postgres.commands().update_remind(remind7, remind1, birthday_record[0])
                        self._postgres.execute(command)

# ------------------------------------------------------------------------------------------
#       PRIVATE METHODS
# ------------------------------------------------------------------------------------------

    @staticmethod
    def _correct_postgres_answer(answer):
        return True if answer and len(answer) > 0 else False

    def _send_create_message(self, update, context):
        account_id = update.effective_user[ACCOUNT_ID]
        language = self._status[account_id][LANGUAGE]
        translate = self._translator.translate
        self._status[account_id][STATUS] = CREATE
        keyboard = [
            [
                InlineKeyboardButton(translate("ФИО", language), callback_data=ADD_FIO),
                InlineKeyboardButton(translate("Дата рождения", language), callback_data=ADD_DATE)
            ],
            [
                InlineKeyboardButton(translate("Поздравление", language), callback_data=ADD_CONGRATULATION),
                InlineKeyboardButton(translate("Пожелания", language), callback_data=ADD_DESIRES)
            ],
            [
                InlineKeyboardButton(translate("Создать", language), callback_data=CREATE)
            ]
        ]
        reply_markup = InlineKeyboardMarkup(keyboard)
        year = self._status[account_id][BIRTHDAY][DATE][YEAR]
        month = self._status[account_id][BIRTHDAY][DATE][MONTH]
        day = self._status[account_id][BIRTHDAY][DATE][DAY]

        context.bot.send_message(
            chat_id=update.effective_message.chat_id,
            text=translate("Давай посмотрим что получилось!", language),
            reply_markup=ReplyKeyboardRemove()
        )
        fio_value = ' '.join(value for value in self._status[account_id][BIRTHDAY][FIO].values() if value)
        congratulation = self._status[account_id][BIRTHDAY][CONGRATULATION]
        congratulation_value = congratulation if congratulation else translate("не задано", language)
        desires = self._status[account_id][BIRTHDAY][DESIRES]
        desires_value = desires if desires else translate("не задано", language)
        context.bot.send_message(
            chat_id=update.effective_message.chat_id,
            text="{text}:\n\n"
                 "<b>{fio}</b>: {fio_value}\n"
                 "<b>{date}</b>: {date_value}\n"
                 "<b>{congratulation}</b>: {congratulation_value}\n"
                 "<b>{desires}</b>: {desires_value}".format(
                  text=translate("Анкета друга", language),
                  fio=translate("ФИО", language),
                  date=translate("Дата рождения", language),
                  congratulation=translate("Поздравление", language),
                  desires=translate("Пожелания", language),
                  fio_value=fio_value,
                  date_value="{0}-{1}-{2}".format(year, month, day),
                  congratulation_value=congratulation_value,
                  desires_value=desires_value),
            reply_markup=reply_markup,
            parse_mode=ParseMode.HTML
        )
        self._update_status(account_id)

    def _it_is_time_for_birthday_after_create(self, update, context):
        account_id = update.effective_user[ACCOUNT_ID]
        language = self._status[account_id][LANGUAGE]
        translate = self._translator.translate
        birthday = self._status[account_id][BIRTHDAY]
        datetime_birthday = datetime(
            int(birthday[DATE][YEAR]),
            int(birthday[DATE][MONTH]),
            int(birthday[DATE][DAY]),
            0, 0, 0
        )
        remind7, remind1 = True, True
        datetime_birthday = datetime_birthday.replace(year=datetime.utcnow().year)
        if datetime.utcnow().timestamp() > datetime_birthday.timestamp():
            datetime_birthday = datetime_birthday.replace(year=datetime.utcnow().year + 1)
        if datetime_birthday.timestamp() - datetime.utcnow().timestamp() <= 24 * 60 * 60:
            context.bot.send_message(
                chat_id=update.callback_query.message.chat_id,
                text=translate("У твоего друга менее чем через сутки день рождения!\\n\\n"
                               "{fio} исполняется {age}!\\n\\n"
                               "Не забудь поздравить именинника и постарайся сделать его день рождения незабываемым! "
                               "Надеюсь, что подарок ты уже приготовил!", language).format(
                         fio=str(" ".join(birthday[FIO].values())).strip(),
                         age=str(datetime.utcnow().year - int(birthday[DATE][YEAR])),
                     )
            )
        elif datetime_birthday.timestamp() - datetime.utcnow().timestamp() <= 7 * 24 * 60 * 60:
            context.bot.send_message(
                chat_id=update.callback_query.message.chat_id,
                text=translate(
                    "У твоего друга менее чем через неделю день рождения!\\n\\n"
                    "{fio} исполнится {age}!\\n\\n" 
                    "Приготовь хороший подарок, надеюсь ты знаешь что бы он хотел! "
                    "Не забудь поздравить именинника и постарайся "
                    "сделать его день рождения незабываемым!", language).format(
                    fio=str(" ".join(birthday[FIO].values())).strip(),
                    age=str(datetime.utcnow().year - int(birthday[DATE][YEAR])),
                )
            )
            remind7 = False
        command = self._postgres.commands().select_specific_birthday(
            account_id=account_id,
            first_name=birthday[FIO][FIRST_NAME],
            last_name=birthday[FIO][LAST_NAME],
            middle_name=birthday[FIO][MIDDLE_NAME]
        )
        specific_birthday = self._postgres.execute(command)
        if self._correct_postgres_answer(specific_birthday):
            command = self._postgres.commands().update_remind(remind7, remind1, specific_birthday[0][0])
            self._postgres.execute(command)

    def _update_status(self, account_id):
        command = self._postgres.commands().update_status(json.dumps(self._status[account_id]), account_id)
        self._postgres.execute(command)

# ------------------------------------------------------------------------------------------
#       CALLBACK QUERY METHODS
# ------------------------------------------------------------------------------------------

    def _language_in_callback_query(self, update, context):
        query_data = update.callback_query.data
        account_id = update.effective_user[ACCOUNT_ID]
        translate = self._translator.translate
        language = query_data
        command = self._postgres.commands().update_language(
            language_code=query_data,
            account_id=account_id
        )
        self._postgres.execute(command)
        self._status[account_id][LANGUAGE] = query_data
        self._status[account_id][STATUS] = NONE
        context.bot.edit_message_text(
            chat_id=update.callback_query.message.chat_id,
            message_id=update.callback_query.message.message_id,
            text=translate("Вы изменили язык на русский!", language)
        )
        self._update_status(account_id)

    def _add_fio_in_callback_query(self, update, context):
        account_id = update.effective_user[ACCOUNT_ID]
        language = self._status[account_id][LANGUAGE]
        translate = self._translator.translate
        self._status[account_id][STATUS] = ADD_FIO_LAST_NAME
        context.bot.send_message(
            chat_id=update.callback_query.message.chat_id,
            text=translate("Введите фамилию друга", language)
        )
        self._update_status(account_id)

    def _add_date_in_callback_query(self, update, context):
        account_id = update.effective_user[ACCOUNT_ID]
        language = self._status[account_id][LANGUAGE]
        translate = self._translator.translate
        self._status[account_id][STATUS] = ADD_DATE_INTERVAL
        start_year = 1930
        keyboard = [
            [
                "{0} - {1}".format(
                    start_year + (2 * i + j) * 12, start_year + (2 * i + j) * 12 + 11
                ) for j in range(2)
            ] for i in range(4)
        ]
        context.bot.send_message(
            chat_id=update.effective_message.chat_id,
            text=translate("Введите интервал даты рождения", language),
            reply_markup=ReplyKeyboardMarkup(
                keyboard=keyboard,
                resize_keyboard=True
            )
        )
        self._update_status(account_id)

    def _add_congratulation_in_callback_query(self, update, context):
        account_id = update.effective_user[ACCOUNT_ID]
        language = self._status[account_id][LANGUAGE]
        translate = self._translator.translate
        self._status[account_id][STATUS] = ADD_CONGRATULATION
        context.bot.send_message(
            chat_id=update.callback_query.message.chat_id,
            text=translate("Введите поздравление для друга", language)
        )
        self._update_status(account_id)

    def _add_desires_in_callback_query(self, update, context):
        account_id = update.effective_user[ACCOUNT_ID]
        language = self._status[account_id][LANGUAGE]
        translate = self._translator.translate
        self._status[account_id][STATUS] = ADD_DESIRES
        context.bot.send_message(
            chat_id=update.callback_query.message.chat_id,
            message_id=update.callback_query.message.message_id,
            text=translate("Введите желания друга", language)
        )
        self._update_status(account_id)

    def _create_in_callback_query(self, update, context):
        account_id = update.effective_user[ACCOUNT_ID]
        language = self._status[account_id][LANGUAGE]
        translate = self._translator.translate
        context.bot.answer_callback_query(callback_query_id=update.callback_query.id)
        birthday = self._status[account_id][BIRTHDAY]
        year = birthday[DATE][YEAR]
        month = birthday[DATE][MONTH]
        day = birthday[DATE][DAY]
        command = self._postgres.commands().insert_birthday(
            last_name=birthday[FIO][LAST_NAME],
            first_name=birthday[FIO][FIRST_NAME],
            middle_name=birthday[FIO][MIDDLE_NAME],
            phone_number=birthday[PHONE_NUMBER],
            user_id=birthday[TELEGRAM_USER_ID],
            date="{0}-{1}-{2}".format(year, month, day),
            congratulation=birthday[CONGRATULATION],
            desires=birthday[DESIRES],
            remind7=True,
            remind1=True,
            account_id=account_id
        )
        self._postgres.execute(command)
        context.bot.edit_message_text(
            chat_id=update.callback_query.message.chat_id,
            message_id=update.callback_query.message.message_id,
            text=translate("Вы успешно добавили информацию о друге!", language)
        )
        self._it_is_time_for_birthday_after_create(update, context)
        self._status[account_id][STATUS] = NONE
        self._status[account_id][BIRTHDAY] = {}
        self._update_status(account_id)

    def _invalid_in_callback_query(self, update, context):
        account_id = update.effective_user[ACCOUNT_ID]
        language = self._status[account_id][LANGUAGE]
        translate = self._translator.translate
        context.bot.edit_message_text(
            chat_id=update.callback_query.message.chat_id,
            message_id=update.callback_query.message.message_id,
            text=translate("К сожалению, я не поддерживаю данное сообщение!", language)
        )

# ------------------------------------------------------------------------------------------
#       OTHER METHODS
# ------------------------------------------------------------------------------------------

    def _add_contact_in_other_messages(self, update, context):
        account_id = update.effective_user[ACCOUNT_ID]
        language = self._status[account_id][LANGUAGE]
        translate = self._translator.translate
        if update.effective_message[ATTACHMENTS_CONTACT]:
            birthday = self._status[account_id][BIRTHDAY]
            first_name = update.effective_message[CONTACT][FIRST_NAME]
            last_name = update.effective_message[CONTACT][LAST_NAME]
            phone_number = update.effective_message[CONTACT][PHONE_NUMBER]
            user_id = update.effective_message[CONTACT][TELEGRAM_USER_ID]
            birthday[FIO][LAST_NAME] = last_name if last_name and len(last_name) > 0 else NONE
            birthday[FIO][FIRST_NAME] = first_name if first_name and len(first_name) > 0 else NONE
            birthday[PHONE_NUMBER] = phone_number if phone_number and len(phone_number) > 0 else NONE
            birthday[TELEGRAM_USER_ID] = user_id if user_id else 0
            self._status[account_id][STATUS] = ADD_DATE_INTERVAL
            start_year = 1930
            keyboard = [
                [
                    "{0} - {1}".format(
                        start_year + (2 * i + j) * 12, start_year + (2 * i + j) * 12 + 11
                    ) for j in range(2)
                ] for i in range(4)
            ]
            context.bot.send_message(
                chat_id=update.effective_message.chat_id,
                text=translate("Введите интервал даты рождения", language),
                reply_markup=ReplyKeyboardMarkup(
                    keyboard=keyboard,
                    resize_keyboard=True
                )
            )
        else:
            context.bot.send_message(
                chat_id=update.effective_message.chat_id,
                text=translate("Будьте так любезны, пришлите мне контакт именинника!", language)
            )
        self._update_status(account_id)

    def _add_date_interval_in_other_messages(self, update, context):
        account_id = update.effective_user[ACCOUNT_ID]
        language = self._status[account_id][LANGUAGE]
        translate = self._translator.translate
        self._status[account_id][STATUS] = ADD_DATE_YEAR
        interval = update.effective_message.text
        begin = int(interval[0:4])
        keyboard = [
            [
                "{0}".format(begin + 3 * i + j) for j in range(3)
            ] for i in range(4)
        ]
        context.bot.send_message(
            chat_id=update.effective_message.chat_id,
            text=translate("Введите год даты рождения", language),
            reply_markup=ReplyKeyboardMarkup(
                keyboard=keyboard,
                resize_keyboard=True
            )
        )
        self._update_status(account_id)

    def _add_date_year_in_other_messages(self, update, context):
        account_id = update.effective_user[ACCOUNT_ID]
        language = self._status[account_id][LANGUAGE]
        translate = self._translator.translate
        self._status[account_id][BIRTHDAY][DATE] = {
            YEAR: NONE,
            MONTH: NONE,
            DAY: NONE
        }
        self._status[account_id][BIRTHDAY][DATE][YEAR] = update.effective_message.text
        self._status[account_id][STATUS] = ADD_DATE_MONTH
        keyboard = [
            [
                translate(MONTH_LIST[3 * i + j], language) for j in range(3)
            ] for i in range(4)
        ]
        context.bot.send_message(
            chat_id=update.effective_message.chat_id,
            text=translate("Введите месяц даты рождения", language),
            reply_markup=ReplyKeyboardMarkup(
                keyboard=keyboard,
                resize_keyboard=True
            )
        )
        self._update_status(account_id)

    def _add_date_month_in_other_messages(self, update, context):
        account_id = update.effective_user[ACCOUNT_ID]
        language = self._status[account_id][LANGUAGE]
        translate = self._translator.translate
        self._status[account_id][STATUS] = ADD_DATE_DAY
        month_arr = [translate(month, language) for month in MONTH_LIST]
        self._status[account_id][BIRTHDAY][DATE][MONTH] = str(month_arr.index(update.effective_message.text) + 1)
        year = int(self._status[account_id][BIRTHDAY][DATE][YEAR])
        month = int(self._status[account_id][BIRTHDAY][DATE][MONTH])
        days = calendar.monthrange(year, month)[1]
        keyboard = [
            [
                str(4 * i + j + 1) for j in range(4) if 4 * i + j + 1 <= days
            ] for i in range(days // 4 + 1)
        ]
        context.bot.send_message(
            chat_id=update.effective_message.chat_id,
            text=translate("Введите день даты рождения", language),
            reply_markup=ReplyKeyboardMarkup(
                keyboard=keyboard,
                resize_keyboard=True
            )
        )
        self._update_status(account_id)

    def _add_date_day_in_other_messages(self, update, context):
        account_id = update.effective_user[ACCOUNT_ID]
        self._status[account_id][BIRTHDAY][DATE][DAY] = update.effective_message.text
        self._send_create_message(update, context)
        self._update_status(account_id)

    def _add_fio_last_name_in_other_messages(self, update, context):
        account_id = update.effective_user[ACCOUNT_ID]
        language = self._status[account_id][LANGUAGE]
        translate = self._translator.translate
        if update.effective_message.text:
            self._status[account_id][STATUS] = ADD_FIO_FIRST_NAME
            self._status[account_id][BIRTHDAY][FIO][LAST_NAME] = update.effective_message.text
            context.bot.send_message(
                chat_id=update.effective_message.chat_id,
                text=translate("Введите имя друга", language)
            )
        else:
            context.bot.send_message(
                chat_id=update.effective_message.chat_id,
                text=translate("Будьте так любезны, введите фамилию друга", language)
            )
        self._update_status(account_id)

    def _add_fio_first_name_in_other_messages(self, update, context):
        account_id = update.effective_user[ACCOUNT_ID]
        language = self._status[account_id][LANGUAGE]
        translate = self._translator.translate
        if update.effective_message.text:
            self._status[account_id][STATUS] = ADD_FIO_MIDDLE_NAME
            self._status[account_id][BIRTHDAY][FIO][FIRST_NAME] = update.effective_message.text
            context.bot.send_message(
                chat_id=update.effective_message.chat_id,
                text=translate("Введите отчество друга", language)
            )
        else:
            context.bot.send_message(
                chat_id=update.effective_message.chat_id,
                text=translate("Будьте так любезны, введите имя друга", language)
            )
        self._update_status(account_id)

    def _add_fio_middle_name_in_other_messages(self, update, context):
        account_id = update.effective_user[ACCOUNT_ID]
        language = self._status[account_id][LANGUAGE]
        translate = self._translator.translate
        if update.effective_message.text:
            self._status[account_id][BIRTHDAY][FIO][MIDDLE_NAME] = update.effective_message.text
            self._send_create_message(update, context)
        else:
            context.bot.send_message(
                chat_id=update.effective_message.chat_id,
                text=translate("Будьте так любезны, введите отчество друга", language)
            )
        self._update_status(account_id)

    def _add_congratulation_in_other_messages(self, update, context):
        account_id = update.effective_user[ACCOUNT_ID]
        language = self._status[account_id][LANGUAGE]
        translate = self._translator.translate
        if update.effective_message.text:
            self._status[account_id][BIRTHDAY][CONGRATULATION] = update.effective_message.text
            self._send_create_message(update, context)
        else:
            context.bot.send_message(
                chat_id=update.effective_message.chat_id,
                text=translate("Будьте так любезны, пришлите мне поздравление для именинника!", language)
            )
        self._update_status(account_id)

    def _add_desires_in_other_messages(self, update, context):
        account_id = update.effective_user[ACCOUNT_ID]
        language = self._status[account_id][LANGUAGE]
        translate = self._translator.translate
        if update.effective_message.text:
            self._status[account_id][BIRTHDAY][DESIRES] = update.effective_message.text
            self._send_create_message(update, context)
        else:
            context.bot.send_message(
                chat_id=update.effective_message.chat_id,
                text=translate("Будьте так любезны, пришлите мне пожелания к подарку для именинника!", language)
            )
        self._update_status(account_id)

    def _invalid_in_other_messages(self, update, context):
        account_id = update.effective_user[ACCOUNT_ID]
        self._status[account_id][STATUS] = CREATE if self._status[account_id][STATUS] == CREATE else NONE

        if update.effective_message[ATTACHMENTS_AUDIO]:
            self._audio_in_other_messages(update, context)
        elif update.effective_message[ATTACHMENTS_DOCUMENT]:
            self._document_in_other_messages(update, context)
        elif update.effective_message[ATTACHMENTS_PHOTO]:
            self._photo_in_other_messages(update, context)
        elif update.effective_message[ATTACHMENTS_STICKER]:
            self._sticker_in_other_messages(update, context)
        elif update.effective_message[ATTACHMENTS_VIDEO]:
            self._video_in_other_messages(update, context)
        elif update.effective_message[ATTACHMENTS_VOICE]:
            self._voice_in_other_messages(update, context)
        elif update.effective_message[ATTACHMENTS_VIDEO_NOTE]:
            self._video_note_in_other_messages(update, context)
        elif update.effective_message[ATTACHMENTS_CONTACT]:
            self._contact_note_in_other_messages(update, context)
        elif update.effective_message[ATTACHMENTS_LOCATION]:
            self._location_in_other_messages(update, context)
        elif update.effective_message[ATTACHMENTS_VENUE]:
            self._venue_in_other_messages(update, context)
        elif update.effective_message[ATTACHMENTS_GAME]:
            self._game_in_other_messages(update, context)
        elif update.effective_message[ATTACHMENTS_ANIMATION]:
            self._animation_in_other_messages(update, context)
        elif update.effective_message[ATTACHMENTS_INVOICE]:
            self._invoice_in_other_messages(update, context)
        elif update.effective_message[ATTACHMENTS_SUCCESSFUL_PAYMENT]:
            self._successful_payment_in_other_messages(update, context)
        else:
            self._unprocessed_other_messages(update, context)
        self._update_status(account_id)

# ------------------------------------------------------------------------------------------
#       ATTACHMENT METHODS
# ------------------------------------------------------------------------------------------

    def _audio_in_other_messages(self, update, context):
        account_id = update.effective_user[ACCOUNT_ID]
        language = self._status[account_id][LANGUAGE]
        translate = self._translator.translate
        context.bot.send_message(
            chat_id=update.effective_message.chat_id,
            text=translate("У вас определенно хороший музыкальный вкус!", language)
        )

    def _document_in_other_messages(self, update, context):
        account_id = update.effective_user[ACCOUNT_ID]
        language = self._status[account_id][LANGUAGE]
        translate = self._translator.translate
        context.bot.send_message(
            chat_id=update.effective_message.chat_id,
            text=translate("Я обязательно прочту ваш документ и напишу рецензию!", language)
        )

    def _photo_in_other_messages(self, update, context):
        account_id = update.effective_user[ACCOUNT_ID]
        language = self._status[account_id][LANGUAGE]
        translate = self._translator.translate
        context.bot.send_message(
            chat_id=update.effective_message.chat_id,
            text=translate("Как вы смеете мне присылать такие фото!", language)
        )

    def _sticker_in_other_messages(self, update, context):
        account_id = update.effective_user[ACCOUNT_ID]
        language = self._status[account_id][LANGUAGE]
        translate = self._translator.translate
        context.bot.send_message(
            chat_id=update.effective_message.chat_id,
            text=translate("У меня есть набор стикеров поинтереснее!", language)
        )

    def _video_in_other_messages(self, update, context):
        account_id = update.effective_user[ACCOUNT_ID]
        language = self._status[account_id][LANGUAGE]
        translate = self._translator.translate
        context.bot.send_message(
            chat_id=update.effective_message.chat_id,
            text=translate("Это видео я отправлю в Роскомнадзор, там с вами разберутся!", language)
        )

    def _voice_in_other_messages(self, update, context):
        account_id = update.effective_user[ACCOUNT_ID]
        language = self._status[account_id][LANGUAGE]
        translate = self._translator.translate
        context.bot.send_message(
            chat_id=update.effective_message.chat_id,
            text=translate("Какой милый голосок! Прочитай мне сказку!", language)
        )

    def _video_note_in_other_messages(self, update, context):
        account_id = update.effective_user[ACCOUNT_ID]
        language = self._status[account_id][LANGUAGE]
        translate = self._translator.translate
        context.bot.send_message(
            chat_id=update.effective_message.chat_id,
            text=translate("Если я буду присылать такие видео как проснусь, то я буду выглядеть намного лучше!", language)
        )

    def _contact_note_in_other_messages(self, update, context):
        account_id = update.effective_user[ACCOUNT_ID]
        language = self._status[account_id][LANGUAGE]
        translate = self._translator.translate
        context.bot.send_message(
            chat_id=update.effective_message.chat_id,
            text=translate("ФСБ проверит твой контакт, я уже передал!", language)
        )

    def _location_in_other_messages(self, update, context):
        account_id = update.effective_user[ACCOUNT_ID]
        language = self._status[account_id][LANGUAGE]
        translate = self._translator.translate
        context.bot.send_message(
            chat_id=update.effective_message.chat_id,
            text=translate("Если там собираются красивые девушки, то я уже выезжаю!", language)
        )

    def _venue_in_other_messages(self, update, context):
        account_id = update.effective_user[ACCOUNT_ID]
        language = self._status[account_id][LANGUAGE]
        translate = self._translator.translate
        context.bot.send_message(
            chat_id=update.effective_message.chat_id,
            text=translate("Там определенно продаются самые вкусные пончики в мире!", language)
        )

    def _game_in_other_messages(self, update, context):
        account_id = update.effective_user[ACCOUNT_ID]
        language = self._status[account_id][LANGUAGE]
        translate = self._translator.translate
        context.bot.send_message(
            chat_id=update.effective_message.chat_id,
            text=translate("Я в такие игры не играю! Я еще маленький!", language)
        )

    def _animation_in_other_messages(self, update, context):
        account_id = update.effective_user[ACCOUNT_ID]
        language = self._status[account_id][LANGUAGE]
        translate = self._translator.translate
        context.bot.send_message(
            chat_id=update.effective_message.chat_id,
            text=translate("Рассмашил так рассмешил! Мне понравилось!", language)
        )

    def _invoice_in_other_messages(self, update, context):
        account_id = update.effective_user[ACCOUNT_ID]
        language = self._status[account_id][LANGUAGE]
        translate = self._translator.translate
        context.bot.send_message(
            chat_id=update.effective_message.chat_id,
            text=translate("Я такого не заказывал! Хочу оформить возврат!", language)
        )

    def _successful_payment_in_other_messages(self, update, context):
        account_id = update.effective_user[ACCOUNT_ID]
        language = self._status[account_id][LANGUAGE]
        translate = self._translator.translate
        context.bot.send_message(
            chat_id=update.effective_message.chat_id,
            text=translate("Если вы отдаете мне это бесплатно, то я готов принять подарок!", language)
        )

    def _unprocessed_other_messages(self, update, context):
        account_id = update.effective_user[ACCOUNT_ID]
        language = self._status[account_id][LANGUAGE]
        translate = self._translator.translate
        context.bot.send_message(
            chat_id=update.effective_message.chat_id,
            text=translate("К сожалению, я не поддерживаю данное сообщение!", language)
        )
Exemplo n.º 19
0
    if abs(diff_percent) > 0.5:
        news_params = {
            "apiKey": NEWS_API_KEY,  # https://newsapi.org
            "qInTitle": COMPANY_NAME,
        }
        news_response = requests.get(NEWS_ENDPOINT, params=news_params)
        articles = news_response.json()["articles"]
        print(articles)
        three_articles = articles[:3]
        print(three_articles)

        formatted_articles = [
            f"{STOCK}: {up_down}{diff_percent}% " \
            f"\nHeadline: {article['title']}. \nBrief: {article['description']}"
            for article in three_articles
        ]

        for article in formatted_articles:
            context.bot.send_message(chat_id=CHAT_ID, text=article)


updater = Updater(BOT_TOKEN,
                  persistence=PicklePersistence(filename='bot_data'))
job_queue = JobQueue()
job_queue.set_dispatcher(updater.dispatcher)
job_queue.run_repeating(callback=send_notify, interval=60)
updater.start_polling()
job_queue.start()
updater.idle()
Exemplo n.º 20
0
from telegram import (ReplyKeyboardMarkup, KeyboardButton,
                      InlineKeyboardMarkup, InlineKeyboardButton,
                      InputMediaPhoto)
from update_stats import update_pages
import logging, os, db

with open('secret.token', 'r') as f:
    TOKEN = f.read().split()[1]

logging.basicConfig(filename='log.log', level=logging.INFO)

updater = Updater(token=TOKEN, use_context=True)
dispatcher = updater.dispatcher

jobQ = JobQueue()
jobQ.set_dispatcher(dispatcher)
jobQ.run_repeating(update_pages, interval=1800, first=1)
jobQ.start()

dbase = db.DB('db.db')


def start(update, context):
    num_of_images = len(os.listdir('./pages'))

    keyboard = [[
        InlineKeyboardButton('⬅', callback_data="p 0"),
        InlineKeyboardButton('🔄', callback_data="p 1"),
        InlineKeyboardButton('➡', callback_data="p 2")
    ],
                [
Exemplo n.º 21
0
class Updater(Generic[CCT, UD, CD, BD]):
    """
    This class, which employs the :class:`telegram.ext.Dispatcher`, provides a frontend to
    :class:`telegram.Bot` to the programmer, so they can focus on coding the bot. Its purpose is to
    receive the updates from Telegram and to deliver them to said dispatcher. It also runs in a
    separate thread, so the user can interact with the bot, for example on the command line. The
    dispatcher supports handlers for different kinds of data: Updates from Telegram, basic text
    commands and even arbitrary types. The updater can be started as a polling service or, for
    production, use a webhook to receive updates. This is achieved using the WebhookServer and
    WebhookHandler classes.

    Note:
        * You must supply either a :attr:`bot` or a :attr:`token` argument.
        * If you supply a :attr:`bot`, you will need to pass :attr:`arbitrary_callback_data`,
          and :attr:`defaults` to the bot instead of the :class:`telegram.ext.Updater`. In this
          case, you'll have to use the class :class:`telegram.ext.ExtBot`.

          .. versionchanged:: 13.6

    Args:
        token (:obj:`str`, optional): The bot's token given by the @BotFather.
        base_url (:obj:`str`, optional): Base_url for the bot.
        base_file_url (:obj:`str`, optional): Base_file_url for the bot.
        workers (:obj:`int`, optional): Amount of threads in the thread pool for functions
            decorated with ``@run_async`` (ignored if `dispatcher` argument is used).
        bot (:class:`telegram.Bot`, optional): A pre-initialized bot instance (ignored if
            `dispatcher` argument is used). If a pre-initialized bot is used, it is the user's
            responsibility to create it using a `Request` instance with a large enough connection
            pool.
        dispatcher (:class:`telegram.ext.Dispatcher`, optional): A pre-initialized dispatcher
            instance. If a pre-initialized dispatcher is used, it is the user's responsibility to
            create it with proper arguments.
        private_key (:obj:`bytes`, optional): Private key for decryption of telegram passport data.
        private_key_password (:obj:`bytes`, optional): Password for above private key.
        user_sig_handler (:obj:`function`, optional): Takes ``signum, frame`` as positional
            arguments. This will be called when a signal is received, defaults are (SIGINT,
            SIGTERM, SIGABRT) settable with :attr:`idle`.
        request_kwargs (:obj:`dict`, optional): Keyword args to control the creation of a
            `telegram.utils.request.Request` object (ignored if `bot` or `dispatcher` argument is
            used). The request_kwargs are very useful for the advanced users who would like to
            control the default timeouts and/or control the proxy used for http communication.
        use_context (:obj:`bool`, optional): If set to :obj:`True` uses the context based callback
            API (ignored if `dispatcher` argument is used). Defaults to :obj:`True`.
            **New users**: set this to :obj:`True`.
        persistence (:class:`telegram.ext.BasePersistence`, optional): The persistence class to
            store data that should be persistent over restarts (ignored if `dispatcher` argument is
            used).
        defaults (:class:`telegram.ext.Defaults`, optional): An object containing default values to
            be used if not set explicitly in the bot methods.
        arbitrary_callback_data (:obj:`bool` | :obj:`int` | :obj:`None`, optional): Whether to
            allow arbitrary objects as callback data for :class:`telegram.InlineKeyboardButton`.
            Pass an integer to specify the maximum number of cached objects. For more details,
            please see our wiki. Defaults to :obj:`False`.

            .. versionadded:: 13.6
        context_types (:class:`telegram.ext.ContextTypes`, optional): Pass an instance
            of :class:`telegram.ext.ContextTypes` to customize the types used in the
            ``context`` interface. If not passed, the defaults documented in
            :class:`telegram.ext.ContextTypes` will be used.

            .. versionadded:: 13.6

    Raises:
        ValueError: If both :attr:`token` and :attr:`bot` are passed or none of them.


    Attributes:
        bot (:class:`telegram.Bot`): The bot used with this Updater.
        user_sig_handler (:obj:`function`): Optional. Function to be called when a signal is
            received.
        update_queue (:obj:`Queue`): Queue for the updates.
        job_queue (:class:`telegram.ext.JobQueue`): Jobqueue for the updater.
        dispatcher (:class:`telegram.ext.Dispatcher`): Dispatcher that handles the updates and
            dispatches them to the handlers.
        running (:obj:`bool`): Indicates if the updater is running.
        persistence (:class:`telegram.ext.BasePersistence`): Optional. The persistence class to
            store data that should be persistent over restarts.
        use_context (:obj:`bool`): Optional. :obj:`True` if using context based callbacks.

    """

    __slots__ = (
        'persistence',
        'dispatcher',
        'user_sig_handler',
        'bot',
        'logger',
        'update_queue',
        'job_queue',
        '__exception_event',
        'last_update_id',
        'running',
        '_request',
        'is_idle',
        'httpd',
        '__lock',
        '__threads',
        '__dict__',
    )

    @overload
    def __init__(
        self: 'Updater[CallbackContext, dict, dict, dict]',
        token: str = None,
        base_url: str = None,
        workers: int = 4,
        bot: Bot = None,
        private_key: bytes = None,
        private_key_password: bytes = None,
        user_sig_handler: Callable = None,
        request_kwargs: Dict[str, Any] = None,
        persistence: 'BasePersistence' = None,  # pylint: disable=E0601
        defaults: 'Defaults' = None,
        use_context: bool = True,
        base_file_url: str = None,
        arbitrary_callback_data: Union[DefaultValue, bool, int,
                                       None] = DEFAULT_FALSE,
    ):
        ...

    @overload
    def __init__(
        self: 'Updater[CCT, UD, CD, BD]',
        token: str = None,
        base_url: str = None,
        workers: int = 4,
        bot: Bot = None,
        private_key: bytes = None,
        private_key_password: bytes = None,
        user_sig_handler: Callable = None,
        request_kwargs: Dict[str, Any] = None,
        persistence: 'BasePersistence' = None,
        defaults: 'Defaults' = None,
        use_context: bool = True,
        base_file_url: str = None,
        arbitrary_callback_data: Union[DefaultValue, bool, int,
                                       None] = DEFAULT_FALSE,
        context_types: ContextTypes[CCT, UD, CD, BD] = None,
    ):
        ...

    @overload
    def __init__(
        self: 'Updater[CCT, UD, CD, BD]',
        user_sig_handler: Callable = None,
        dispatcher: Dispatcher[CCT, UD, CD, BD] = None,
    ):
        ...

    def __init__(  # type: ignore[no-untyped-def,misc]
        self,
        token: str = None,
        base_url: str = None,
        workers: int = 4,
        bot: Bot = None,
        private_key: bytes = None,
        private_key_password: bytes = None,
        user_sig_handler: Callable = None,
        request_kwargs: Dict[str, Any] = None,
        persistence: 'BasePersistence' = None,
        defaults: 'Defaults' = None,
        use_context: bool = True,
        dispatcher=None,
        base_file_url: str = None,
        arbitrary_callback_data: Union[DefaultValue, bool, int,
                                       None] = DEFAULT_FALSE,
        context_types: ContextTypes[CCT, UD, CD, BD] = None,
    ):

        if defaults and bot:
            warnings.warn(
                'Passing defaults to an Updater has no effect when a Bot is passed '
                'as well. Pass them to the Bot instead.',
                TelegramDeprecationWarning,
                stacklevel=2,
            )
        if arbitrary_callback_data is not DEFAULT_FALSE and bot:
            warnings.warn(
                'Passing arbitrary_callback_data to an Updater has no '
                'effect when a Bot is passed as well. Pass them to the Bot instead.',
                stacklevel=2,
            )

        if dispatcher is None:
            if (token is None) and (bot is None):
                raise ValueError('`token` or `bot` must be passed')
            if (token is not None) and (bot is not None):
                raise ValueError('`token` and `bot` are mutually exclusive')
            if (private_key is not None) and (bot is not None):
                raise ValueError(
                    '`bot` and `private_key` are mutually exclusive')
        else:
            if bot is not None:
                raise ValueError(
                    '`dispatcher` and `bot` are mutually exclusive')
            if persistence is not None:
                raise ValueError(
                    '`dispatcher` and `persistence` are mutually exclusive')
            if use_context != dispatcher.use_context:
                raise ValueError(
                    '`dispatcher` and `use_context` are mutually exclusive')
            if context_types is not None:
                raise ValueError(
                    '`dispatcher` and `context_types` are mutually exclusive')
            if workers is not None:
                raise ValueError(
                    '`dispatcher` and `workers` are mutually exclusive')

        self.logger = logging.getLogger(__name__)
        self._request = None

        if dispatcher is None:
            con_pool_size = workers + 4

            if bot is not None:
                self.bot = bot
                if bot.request.con_pool_size < con_pool_size:
                    self.logger.warning(
                        'Connection pool of Request object is smaller than optimal value (%s)',
                        con_pool_size,
                    )
            else:
                # we need a connection pool the size of:
                # * for each of the workers
                # * 1 for Dispatcher
                # * 1 for polling Updater (even if webhook is used, we can spare a connection)
                # * 1 for JobQueue
                # * 1 for main thread
                if request_kwargs is None:
                    request_kwargs = {}
                if 'con_pool_size' not in request_kwargs:
                    request_kwargs['con_pool_size'] = con_pool_size
                self._request = Request(**request_kwargs)
                self.bot = ExtBot(
                    token,  # type: ignore[arg-type]
                    base_url,
                    base_file_url=base_file_url,
                    request=self._request,
                    private_key=private_key,
                    private_key_password=private_key_password,
                    defaults=defaults,
                    arbitrary_callback_data=(
                        False  # type: ignore[arg-type]
                        if arbitrary_callback_data is DEFAULT_FALSE else
                        arbitrary_callback_data),
                )
            self.update_queue: Queue = Queue()
            self.job_queue = JobQueue()
            self.__exception_event = Event()
            self.persistence = persistence
            self.dispatcher = Dispatcher(
                self.bot,
                self.update_queue,
                job_queue=self.job_queue,
                workers=workers,
                exception_event=self.__exception_event,
                persistence=persistence,
                use_context=use_context,
                context_types=context_types,
            )
            self.job_queue.set_dispatcher(self.dispatcher)
        else:
            con_pool_size = dispatcher.workers + 4

            self.bot = dispatcher.bot
            if self.bot.request.con_pool_size < con_pool_size:
                self.logger.warning(
                    'Connection pool of Request object is smaller than optimal value (%s)',
                    con_pool_size,
                )
            self.update_queue = dispatcher.update_queue
            self.__exception_event = dispatcher.exception_event
            self.persistence = dispatcher.persistence
            self.job_queue = dispatcher.job_queue
            self.dispatcher = dispatcher

        self.user_sig_handler = user_sig_handler
        self.last_update_id = 0
        self.running = False
        self.is_idle = False
        self.httpd = None
        self.__lock = Lock()
        self.__threads: List[Thread] = []

    def __setattr__(self, key: str, value: object) -> None:
        if key.startswith('__'):
            key = f"_{self.__class__.__name__}{key}"
        if issubclass(self.__class__,
                      Updater) and self.__class__ is not Updater:
            object.__setattr__(self, key, value)
            return
        set_new_attribute_deprecated(self, key, value)

    def _init_thread(self, target: Callable, name: str, *args: object,
                     **kwargs: object) -> None:
        thr = Thread(
            target=self._thread_wrapper,
            name=f"Bot:{self.bot.id}:{name}",
            args=(target, ) + args,
            kwargs=kwargs,
        )
        thr.start()
        self.__threads.append(thr)

    def _thread_wrapper(self, target: Callable, *args: object,
                        **kwargs: object) -> None:
        thr_name = current_thread().name
        self.logger.debug('%s - started', thr_name)
        try:
            target(*args, **kwargs)
        except Exception:
            self.__exception_event.set()
            self.logger.exception('unhandled exception in %s', thr_name)
            raise
        self.logger.debug('%s - ended', thr_name)

    def start_polling(
        self,
        poll_interval: float = 0.0,
        timeout: float = 10,
        clean: bool = None,
        bootstrap_retries: int = -1,
        read_latency: float = 2.0,
        allowed_updates: List[str] = None,
        drop_pending_updates: bool = None,
    ) -> Optional[Queue]:
        """Starts polling updates from Telegram.

        Args:
            poll_interval (:obj:`float`, optional): Time to wait between polling updates from
                Telegram in seconds. Default is ``0.0``.
            timeout (:obj:`float`, optional): Passed to :meth:`telegram.Bot.get_updates`.
            drop_pending_updates (:obj:`bool`, optional): Whether to clean any pending updates on
                Telegram servers before actually starting to poll. Default is :obj:`False`.

                .. versionadded :: 13.4
            clean (:obj:`bool`, optional): Alias for ``drop_pending_updates``.

                .. deprecated:: 13.4
                    Use ``drop_pending_updates`` instead.
            bootstrap_retries (:obj:`int`, optional): Whether the bootstrapping phase of the
                :class:`telegram.ext.Updater` will retry on failures on the Telegram server.

                * < 0 - retry indefinitely (default)
                *   0 - no retries
                * > 0 - retry up to X times

            allowed_updates (List[:obj:`str`], optional): Passed to
                :meth:`telegram.Bot.get_updates`.
            read_latency (:obj:`float` | :obj:`int`, optional): Grace time in seconds for receiving
                the reply from server. Will be added to the ``timeout`` value and used as the read
                timeout from server (Default: ``2``).

        Returns:
            :obj:`Queue`: The update queue that can be filled from the main thread.

        """
        if (clean is not None) and (drop_pending_updates is not None):
            raise TypeError(
                '`clean` and `drop_pending_updates` are mutually exclusive.')

        if clean is not None:
            warnings.warn(
                'The argument `clean` of `start_polling` is deprecated. Please use '
                '`drop_pending_updates` instead.',
                category=TelegramDeprecationWarning,
                stacklevel=2,
            )

        drop_pending_updates = drop_pending_updates if drop_pending_updates is not None else clean

        with self.__lock:
            if not self.running:
                self.running = True

                # Create & start threads
                self.job_queue.start()
                dispatcher_ready = Event()
                polling_ready = Event()
                self._init_thread(self.dispatcher.start,
                                  "dispatcher",
                                  ready=dispatcher_ready)
                self._init_thread(
                    self._start_polling,
                    "updater",
                    poll_interval,
                    timeout,
                    read_latency,
                    bootstrap_retries,
                    drop_pending_updates,
                    allowed_updates,
                    ready=polling_ready,
                )

                self.logger.debug(
                    'Waiting for Dispatcher and polling to start')
                dispatcher_ready.wait()
                polling_ready.wait()

                # Return the update queue so the main thread can insert updates
                return self.update_queue
            return None

    def start_webhook(
        self,
        listen: str = '127.0.0.1',
        port: int = 80,
        url_path: str = '',
        cert: str = None,
        key: str = None,
        clean: bool = None,
        bootstrap_retries: int = 0,
        webhook_url: str = None,
        allowed_updates: List[str] = None,
        force_event_loop: bool = None,
        drop_pending_updates: bool = None,
        ip_address: str = None,
        max_connections: int = 40,
    ) -> Optional[Queue]:
        """
        Starts a small http server to listen for updates via webhook. If :attr:`cert`
        and :attr:`key` are not provided, the webhook will be started directly on
        http://listen:port/url_path, so SSL can be handled by another
        application. Else, the webhook will be started on
        https://listen:port/url_path. Also calls :meth:`telegram.Bot.set_webhook` as required.

        .. versionchanged:: 13.4
            :meth:`start_webhook` now *always* calls :meth:`telegram.Bot.set_webhook`, so pass
            ``webhook_url`` instead of calling ``updater.bot.set_webhook(webhook_url)`` manually.

        Args:
            listen (:obj:`str`, optional): IP-Address to listen on. Default ``127.0.0.1``.
            port (:obj:`int`, optional): Port the bot should be listening on. Default ``80``.
            url_path (:obj:`str`, optional): Path inside url.
            cert (:obj:`str`, optional): Path to the SSL certificate file.
            key (:obj:`str`, optional): Path to the SSL key file.
            drop_pending_updates (:obj:`bool`, optional): Whether to clean any pending updates on
                Telegram servers before actually starting to poll. Default is :obj:`False`.

                .. versionadded :: 13.4
            clean (:obj:`bool`, optional): Alias for ``drop_pending_updates``.

                .. deprecated:: 13.4
                    Use ``drop_pending_updates`` instead.
            bootstrap_retries (:obj:`int`, optional): Whether the bootstrapping phase of the
                :class:`telegram.ext.Updater` will retry on failures on the Telegram server.

                * < 0 - retry indefinitely (default)
                *   0 - no retries
                * > 0 - retry up to X times

            webhook_url (:obj:`str`, optional): Explicitly specify the webhook url. Useful behind
                NAT, reverse proxy, etc. Default is derived from ``listen``, ``port`` &
                ``url_path``.
            ip_address (:obj:`str`, optional): Passed to :meth:`telegram.Bot.set_webhook`.

                .. versionadded :: 13.4
            allowed_updates (List[:obj:`str`], optional): Passed to
                :meth:`telegram.Bot.set_webhook`.
            force_event_loop (:obj:`bool`, optional): Legacy parameter formerly used for a
                workaround on Windows + Python 3.8+. No longer has any effect.

                .. deprecated:: 13.6
                   Since version 13.6, ``tornade>=6.1`` is required, which resolves the former
                   issue.

            max_connections (:obj:`int`, optional): Passed to
                :meth:`telegram.Bot.set_webhook`.

                .. versionadded:: 13.6

        Returns:
            :obj:`Queue`: The update queue that can be filled from the main thread.

        """
        if (clean is not None) and (drop_pending_updates is not None):
            raise TypeError(
                '`clean` and `drop_pending_updates` are mutually exclusive.')

        if clean is not None:
            warnings.warn(
                'The argument `clean` of `start_webhook` is deprecated. Please use '
                '`drop_pending_updates` instead.',
                category=TelegramDeprecationWarning,
                stacklevel=2,
            )

        if force_event_loop is not None:
            warnings.warn(
                'The argument `force_event_loop` of `start_webhook` is deprecated and no longer '
                'has any effect.',
                category=TelegramDeprecationWarning,
                stacklevel=2,
            )

        drop_pending_updates = drop_pending_updates if drop_pending_updates is not None else clean

        with self.__lock:
            if not self.running:
                self.running = True

                # Create & start threads
                webhook_ready = Event()
                dispatcher_ready = Event()
                self.job_queue.start()
                self._init_thread(self.dispatcher.start, "dispatcher",
                                  dispatcher_ready)
                self._init_thread(
                    self._start_webhook,
                    "updater",
                    listen,
                    port,
                    url_path,
                    cert,
                    key,
                    bootstrap_retries,
                    drop_pending_updates,
                    webhook_url,
                    allowed_updates,
                    ready=webhook_ready,
                    ip_address=ip_address,
                    max_connections=max_connections,
                )

                self.logger.debug(
                    'Waiting for Dispatcher and Webhook to start')
                webhook_ready.wait()
                dispatcher_ready.wait()

                # Return the update queue so the main thread can insert updates
                return self.update_queue
            return None

    @no_type_check
    def _start_polling(
        self,
        poll_interval,
        timeout,
        read_latency,
        bootstrap_retries,
        drop_pending_updates,
        allowed_updates,
        ready=None,
    ):  # pragma: no cover
        # Thread target of thread 'updater'. Runs in background, pulls
        # updates from Telegram and inserts them in the update queue of the
        # Dispatcher.

        self.logger.debug('Updater thread started (polling)')

        self._bootstrap(
            bootstrap_retries,
            drop_pending_updates=drop_pending_updates,
            webhook_url='',
            allowed_updates=None,
        )

        self.logger.debug('Bootstrap done')

        def polling_action_cb():
            updates = self.bot.get_updates(
                self.last_update_id,
                timeout=timeout,
                read_latency=read_latency,
                allowed_updates=allowed_updates,
            )

            if updates:
                if not self.running:
                    self.logger.debug(
                        'Updates ignored and will be pulled again on restart')
                else:
                    for update in updates:
                        self.update_queue.put(update)
                    self.last_update_id = updates[-1].update_id + 1

            return True

        def polling_onerr_cb(exc):
            # Put the error into the update queue and let the Dispatcher
            # broadcast it
            self.update_queue.put(exc)

        if ready is not None:
            ready.set()

        self._network_loop_retry(polling_action_cb, polling_onerr_cb,
                                 'getting Updates', poll_interval)

    @no_type_check
    def _network_loop_retry(self, action_cb, onerr_cb, description, interval):
        """Perform a loop calling `action_cb`, retrying after network errors.

        Stop condition for loop: `self.running` evaluates :obj:`False` or return value of
        `action_cb` evaluates :obj:`False`.

        Args:
            action_cb (:obj:`callable`): Network oriented callback function to call.
            onerr_cb (:obj:`callable`): Callback to call when TelegramError is caught. Receives the
                exception object as a parameter.
            description (:obj:`str`): Description text to use for logs and exception raised.
            interval (:obj:`float` | :obj:`int`): Interval to sleep between each call to
                `action_cb`.

        """
        self.logger.debug('Start network loop retry %s', description)
        cur_interval = interval
        while self.running:
            try:
                if not action_cb():
                    break
            except RetryAfter as exc:
                self.logger.info('%s', exc)
                cur_interval = 0.5 + exc.retry_after
            except TimedOut as toe:
                self.logger.debug('Timed out %s: %s', description, toe)
                # If failure is due to timeout, we should retry asap.
                cur_interval = 0
            except InvalidToken as pex:
                self.logger.error('Invalid token; aborting')
                raise pex
            except TelegramError as telegram_exc:
                self.logger.error('Error while %s: %s', description,
                                  telegram_exc)
                onerr_cb(telegram_exc)
                cur_interval = self._increase_poll_interval(cur_interval)
            else:
                cur_interval = interval

            if cur_interval:
                sleep(cur_interval)

    @staticmethod
    def _increase_poll_interval(current_interval: float) -> float:
        # increase waiting times on subsequent errors up to 30secs
        if current_interval == 0:
            current_interval = 1
        elif current_interval < 30:
            current_interval *= 1.5
        else:
            current_interval = min(30.0, current_interval)
        return current_interval

    @no_type_check
    def _start_webhook(
        self,
        listen,
        port,
        url_path,
        cert,
        key,
        bootstrap_retries,
        drop_pending_updates,
        webhook_url,
        allowed_updates,
        ready=None,
        ip_address=None,
        max_connections: int = 40,
    ):
        self.logger.debug('Updater thread started (webhook)')

        # Note that we only use the SSL certificate for the WebhookServer, if the key is also
        # present. This is because the WebhookServer may not actually be in charge of performing
        # the SSL handshake, e.g. in case a reverse proxy is used
        use_ssl = cert is not None and key is not None

        if not url_path.startswith('/'):
            url_path = f'/{url_path}'

        # Create Tornado app instance
        app = WebhookAppClass(url_path, self.bot, self.update_queue)

        # Form SSL Context
        # An SSLError is raised if the private key does not match with the certificate
        if use_ssl:
            try:
                ssl_ctx = ssl.create_default_context(ssl.Purpose.CLIENT_AUTH)
                ssl_ctx.load_cert_chain(cert, key)
            except ssl.SSLError as exc:
                raise TelegramError('Invalid SSL Certificate') from exc
        else:
            ssl_ctx = None

        # Create and start server
        self.httpd = WebhookServer(listen, port, app, ssl_ctx)

        if not webhook_url:
            webhook_url = self._gen_webhook_url(listen, port, url_path)

        # We pass along the cert to the webhook if present.
        cert_file = open(cert, 'rb') if cert is not None else None
        self._bootstrap(
            max_retries=bootstrap_retries,
            drop_pending_updates=drop_pending_updates,
            webhook_url=webhook_url,
            allowed_updates=allowed_updates,
            cert=cert_file,
            ip_address=ip_address,
            max_connections=max_connections,
        )
        if cert_file is not None:
            cert_file.close()

        self.httpd.serve_forever(ready=ready)

    @staticmethod
    def _gen_webhook_url(listen: str, port: int, url_path: str) -> str:
        return f'https://{listen}:{port}{url_path}'

    @no_type_check
    def _bootstrap(
        self,
        max_retries,
        drop_pending_updates,
        webhook_url,
        allowed_updates,
        cert=None,
        bootstrap_interval=5,
        ip_address=None,
        max_connections: int = 40,
    ):
        retries = [0]

        def bootstrap_del_webhook():
            self.logger.debug('Deleting webhook')
            if drop_pending_updates:
                self.logger.debug(
                    'Dropping pending updates from Telegram server')
            self.bot.delete_webhook(drop_pending_updates=drop_pending_updates)
            return False

        def bootstrap_set_webhook():
            self.logger.debug('Setting webhook')
            if drop_pending_updates:
                self.logger.debug(
                    'Dropping pending updates from Telegram server')
            self.bot.set_webhook(
                url=webhook_url,
                certificate=cert,
                allowed_updates=allowed_updates,
                ip_address=ip_address,
                drop_pending_updates=drop_pending_updates,
                max_connections=max_connections,
            )
            return False

        def bootstrap_onerr_cb(exc):
            if not isinstance(exc, Unauthorized) and (
                    max_retries < 0 or retries[0] < max_retries):
                retries[0] += 1
                self.logger.warning(
                    'Failed bootstrap phase; try=%s max_retries=%s',
                    retries[0], max_retries)
            else:
                self.logger.error(
                    'Failed bootstrap phase after %s retries (%s)', retries[0],
                    exc)
                raise exc

        # Dropping pending updates from TG can be efficiently done with the drop_pending_updates
        # parameter of delete/start_webhook, even in the case of polling. Also we want to make
        # sure that no webhook is configured in case of polling, so we just always call
        # delete_webhook for polling
        if drop_pending_updates or not webhook_url:
            self._network_loop_retry(
                bootstrap_del_webhook,
                bootstrap_onerr_cb,
                'bootstrap del webhook',
                bootstrap_interval,
            )
            retries[0] = 0

        # Restore/set webhook settings, if needed. Again, we don't know ahead if a webhook is set,
        # so we set it anyhow.
        if webhook_url:
            self._network_loop_retry(
                bootstrap_set_webhook,
                bootstrap_onerr_cb,
                'bootstrap set webhook',
                bootstrap_interval,
            )

    def stop(self) -> None:
        """Stops the polling/webhook thread, the dispatcher and the job queue."""
        self.job_queue.stop()
        with self.__lock:
            if self.running or self.dispatcher.has_running_threads:
                self.logger.debug('Stopping Updater and Dispatcher...')

                self.running = False

                self._stop_httpd()
                self._stop_dispatcher()
                self._join_threads()

                # Stop the Request instance only if it was created by the Updater
                if self._request:
                    self._request.stop()

    @no_type_check
    def _stop_httpd(self) -> None:
        if self.httpd:
            self.logger.debug(
                'Waiting for current webhook connection to be '
                'closed... Send a Telegram message to the bot to exit '
                'immediately.')
            self.httpd.shutdown()
            self.httpd = None

    @no_type_check
    def _stop_dispatcher(self) -> None:
        self.logger.debug('Requesting Dispatcher to stop...')
        self.dispatcher.stop()

    @no_type_check
    def _join_threads(self) -> None:
        for thr in self.__threads:
            self.logger.debug('Waiting for %s thread to end', thr.name)
            thr.join()
            self.logger.debug('%s thread has ended', thr.name)
        self.__threads = []

    @no_type_check
    def _signal_handler(self, signum, frame) -> None:
        self.is_idle = False
        if self.running:
            self.logger.info('Received signal %s (%s), stopping...', signum,
                             get_signal_name(signum))
            if self.persistence:
                # Update user_data, chat_data and bot_data before flushing
                self.dispatcher.update_persistence()
                self.persistence.flush()
            self.stop()
            if self.user_sig_handler:
                self.user_sig_handler(signum, frame)
        else:
            self.logger.warning('Exiting immediately!')
            # pylint: disable=C0415,W0212
            import os

            os._exit(1)

    def idle(
        self, stop_signals: Union[List, Tuple] = (SIGINT, SIGTERM, SIGABRT)
    ) -> None:
        """Blocks until one of the signals are received and stops the updater.

        Args:
            stop_signals (:obj:`list` | :obj:`tuple`): List containing signals from the signal
                module that should be subscribed to. :meth:`Updater.stop()` will be called on
                receiving one of those signals. Defaults to (``SIGINT``, ``SIGTERM``, ``SIGABRT``).

        """
        for sig in stop_signals:
            signal(sig, self._signal_handler)

        self.is_idle = True

        while self.is_idle:
            sleep(1)