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
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
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)
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()
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()
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
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()
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()
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
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()
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()
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()
def job_queue(bot, _dp): jq = JobQueue() jq.set_dispatcher(_dp) jq.start() yield jq jq.stop()
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)
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()
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()
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)
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) )
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()
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") ], [
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)