def __init__(self, token=None, base_url=None, workers=4, bot=None, job_queue_tick_interval=1.0): 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 bot is not None: self.bot = bot else: self.bot = Bot(token, base_url) self.update_queue = Queue() self.job_queue = JobQueue(self.bot, job_queue_tick_interval) self.__exception_event = Event() self.dispatcher = Dispatcher(self.bot, self.update_queue, workers, self.__exception_event) self.last_update_id = 0 self.logger = logging.getLogger(__name__) self.running = False self.is_idle = False self.httpd = None self.__lock = Lock() self.__threads = [] """:type: list[Thread]"""
def __init__(self, bot, dispatcher, database_handler, token): self.dispatcher = dispatcher # queue for async jobs self.job_queue = JobQueue(bot) # Where to get pictures from: local filesystem(local) or Dropbox storage (DB) self.pic_source = sr["pic_source"] self.database_handler = database_handler super(UserCommandHandler, self).__init__(token, self.database_handler) self._addHandlers() if self.pic_source == "DB": self.DB_file_updater_thread = None # a thread that updates files self.dropbox_handler = DropboxHandler(self.database_handler) self._updateDBFiles() elif self.pic_source == "local": self.local_cleaner_job = None self._startLocalCleanerJob() self._initializeSubscriptionJobs()
def __init__(self, token=None, base_url=None, workers=4, bot=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 bot is not None: self.bot = bot 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 self._request = Request(con_pool_size=workers + 4) self.bot = Bot(token, base_url, request=self._request) self.update_queue = Queue() self.job_queue = JobQueue(self.bot) self.__exception_event = Event() self.dispatcher = Dispatcher( self.bot, self.update_queue, job_queue=self.job_queue, workers=workers, exception_event=self.__exception_event) self.last_update_id = 0 self.logger = logging.getLogger(__name__) self.running = False self.is_idle = False self.httpd = None self.__lock = Lock() self.__threads = [] """:type: list[Thread]"""
def initialize_participants(job_queue: JobQueue): user_map = DataSet() try: # Todo: auto-initalize function db = sqlite3.connect('survey/participants.db') cursor = db.cursor() cursor.execute("SELECT * FROM participants ORDER BY (ID)") participants = cursor.fetchall() # print(participants) for row in participants: user = Participant(row[1], init=False) user.conditions_ = pickle.loads(row[2]) user.data_set_ = pickle.loads(row[0]) user.timezone_ = row[3] user.country_ = row[4] user.gender_ = row[5] user.language_ = row[6] user.question_ = row[7] user.age_ = row[8] user.day_ = row[9] user.q_idle_ = row[10] user.active_ = row[11] user.block_ = row[12] user.pointer_ = row[13] user_map.participants[row[1]] = user if user.language_ != '': q_set = user_map.return_question_set_by_language(user.language_) user.q_set_ = q_set if user.country_ != '' and user.timezone_ != '' and user.gender_ != '': user.set_next_block() next_day = user.set_next_block() if next_day is None and user.active_ and user.pointer_ > -1: finished(user, job_queue) continue element = user.next_block[2] day_offset = next_day - user.day_ time_t = calc_block_time(element["time"]) due = calc_delta_t(time_t, day_offset, user.timezone_) debug('QUEUE', 'next block in ' + str(due) + ' seconds. User: ' + str(user.chat_id_), log=True) new_job = Job(queue_next, due, repeat=False, context=[user, job_queue]) job_queue.put(new_job) except sqlite3.Error as error: print(error) return user_map
def __init__(self, token=None, base_url=None, workers=4, bot=None, user_sig_handler=None, request_kwargs=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') 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.bot) self.__exception_event = Event() self.dispatcher = Dispatcher( self.bot, self.update_queue, job_queue=self.job_queue, workers=workers, exception_event=self.__exception_event) self.last_update_id = 0 self.running = False self.is_idle = False self.httpd = None self.__lock = Lock() self.__threads = []
def job_queue(bot, _dp): jq = JobQueue() jq.set_dispatcher(_dp) jq.start() yield jq jq.stop()
class Updater(object): """ This class, which employs the Dispatcher class, provides a frontend to 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: Args: token (Optional[str]): The bot's token given by the @BotFather base_url (Optional[str]): workers (Optional[int]): Amount of threads in the thread pool for functions decorated with @run_async bot (Optional[Bot]): A pre-initialized bot instance. If a pre-initizlied bot is used, it is the user's responsibility to create it using a `Request` instance with a large enough connection pool. request_kwargs (Optional[dict]): Keyword args to control the creation of a request object (ignored if `bot` argument is used). Raises: ValueError: If both `token` and `bot` are passed or none of them. """ _request = None def __init__(self, token=None, base_url=None, workers=4, bot=None, request_kwargs=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 bot is not None: self.bot = bot 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'] = workers + 4 self._request = Request(**request_kwargs) self.bot = Bot(token, base_url, request=self._request) self.update_queue = Queue() self.job_queue = JobQueue(self.bot) self.__exception_event = Event() self.dispatcher = Dispatcher(self.bot, self.update_queue, job_queue=self.job_queue, workers=workers, exception_event=self.__exception_event) self.last_update_id = 0 self.logger = logging.getLogger(__name__) self.running = False self.is_idle = False self.httpd = None self.__lock = Lock() self.__threads = [] """:type: list[Thread]""" 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') raise self.logger.debug('{0} - ended'.format(thr_name)) def start_polling(self, poll_interval=0.0, timeout=10, network_delay=None, clean=False, bootstrap_retries=0, read_latency=2.): """ Starts polling updates from Telegram. Args: poll_interval (Optional[float]): Time to wait between polling updates from Telegram in seconds. Default is 0.0 timeout (Optional[float]): Passed to Bot.getUpdates network_delay: Deprecated. Will be honoured as `read_latency` for a while but will be removed in the future. clean (Optional[bool]): Whether to clean any pending updates on Telegram servers before actually starting to poll. Default is False. bootstrap_retries (Optional[int]): Whether the bootstrapping phase of the `Updater` will retry on failures on the Telegram server. | < 0 - retry indefinitely | 0 - no retries (default) | > 0 - retry up to X times read_latency (Optional[float|int]): 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: Queue: The update queue that can be filled from the main thread """ if network_delay is not None: warnings.warn( 'network_delay is deprecated, use read_latency instead') read_latency = network_delay 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_polling, "updater", poll_interval, timeout, read_latency, bootstrap_retries, clean) # 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): """ 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 (Optional[str]): IP-Address to listen on port (Optional[int]): Port the bot should be listening on url_path (Optional[str]): Path inside url cert (Optional[str]): Path to the SSL certificate file key (Optional[str]): Path to the SSL key file clean (Optional[bool]): Whether to clean any pending updates on Telegram servers before actually starting the webhook. Default is False. bootstrap_retries (Optional[int[): Whether the bootstrapping phase of the `Updater` will retry on failures on the Telegram server. | < 0 - retry indefinitely | 0 - no retries (default) | > 0 - retry up to X times webhook_url (Optional[str]): Explicitly specifiy the webhook url. Useful behind NAT, reverse proxy, etc. Default is derived from `listen`, `port` & `url_path`. Returns: 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) # 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): """ Thread target of thread 'updater'. Runs in background, pulls updates from Telegram and inserts them in the update queue of the Dispatcher. """ cur_interval = poll_interval self.logger.debug('Updater thread started') self._bootstrap(bootstrap_retries, clean=clean, webhook_url='') while self.running: try: updates = self.bot.getUpdates(self.last_update_id, timeout=timeout, read_latency=read_latency) except RetryAfter as e: self.logger.info(str(e)) cur_interval = 0.5 + e.retry_after except TelegramError as te: self.logger.error( "Error while getting Updates: {0}".format(te)) # Put the error into the update queue and let the Dispatcher # broadcast it self.update_queue.put(te) cur_interval = self._increase_poll_interval(cur_interval) else: if not self.running: if len(updates) > 0: self.logger.debug('Updates ignored and will be pulled ' 'again on restart.') break if updates: for update in updates: self.update_queue.put(update) self.last_update_id = updates[-1].update_id + 1 cur_interval = poll_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): self.logger.debug('Updater thread started') 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')) 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, cert=None): retries = 0 while 1: try: if clean: # Disable webhook for cleaning self.bot.setWebhook(webhook_url='') self._clean_updates() sleep(1) self.bot.setWebhook(webhook_url=webhook_url, certificate=cert) except (Unauthorized, InvalidToken): raise except TelegramError: msg = 'error in bootstrap phase; try={0} max_retries={1}'.format( retries, max_retries) if max_retries < 0 or retries < max_retries: self.logger.warning(msg) retries += 1 else: self.logger.exception(msg) raise else: break sleep(1) def _clean_updates(self): self.logger.debug('Cleaning updates from Telegram server') updates = self.bot.getUpdates() while updates: updates = self.bot.getUpdates(updates[-1].update_id + 1) 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.stop() 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: 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)
def __init__(self, token=None, base_url=None, workers=4, bot=None, private_key=None, private_key_password=None, user_sig_handler=None, request_kwargs=None, persistence=None, defaults=None, use_context=False, dispatcher=None, base_file_url=None): 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, 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() 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 = [] # Just for passing to WebhookAppClass self._default_quote = defaults.quote if defaults else None
def init(job_queue: JobQueue): job_queue.run_once(callback=boot_job, when=1)
class JobQueueTest(BaseTest, unittest.TestCase): """ This object represents Tests for Updater, Dispatcher, WebhookServer and WebhookHandler """ def setUp(self): self.jq = JobQueue(MockBot('jobqueue_test')) self.jq.start() self.result = 0 self.job_time = 0 def tearDown(self): if self.jq is not None: self.jq.stop() def job1(self, bot, job): self.result += 1 def job2(self, bot, job): raise Exception("Test Error") def job3(self, bot, job): self.result += 1 job.schedule_removal() def job4(self, bot, job): self.result += job.context def job5(self, bot, job): self.job_time = time.time() def test_basic(self): self.jq.put(Job(self.job1, 0.1)) sleep(1.5) self.assertGreaterEqual(self.result, 10) def test_job_with_context(self): self.jq.put(Job(self.job4, 0.1, context=5)) sleep(1.5) self.assertGreaterEqual(self.result, 50) def test_noRepeat(self): self.jq.put(Job(self.job1, 0.1, repeat=False)) sleep(0.5) self.assertEqual(1, self.result) def test_nextT(self): self.jq.put(Job(self.job1, 0.1), next_t=0.5) sleep(0.45) self.assertEqual(0, self.result) sleep(0.1) self.assertEqual(1, self.result) def test_multiple(self): self.jq.put(Job(self.job1, 0.1, repeat=False)) self.jq.put(Job(self.job1, 0.2, repeat=False)) self.jq.put(Job(self.job1, 0.4)) sleep(1) self.assertEqual(4, self.result) def test_disabled(self): j0 = Job(self.job1, 0.1) j1 = Job(self.job1, 0.2) self.jq.put(j0) self.jq.put(Job(self.job1, 0.4)) self.jq.put(j1) j0.enabled = False j1.enabled = False sleep(1) self.assertEqual(2, self.result) def test_schedule_removal(self): j0 = Job(self.job1, 0.1) j1 = Job(self.job1, 0.2) self.jq.put(j0) self.jq.put(Job(self.job1, 0.4)) self.jq.put(j1) j0.schedule_removal() j1.schedule_removal() sleep(1) self.assertEqual(2, self.result) def test_schedule_removal_from_within(self): self.jq.put(Job(self.job1, 0.4)) self.jq.put(Job(self.job3, 0.2)) sleep(1) self.assertEqual(3, self.result) def test_longer_first(self): self.jq.put(Job(self.job1, 0.2, repeat=False)) self.jq.put(Job(self.job1, 0.1, repeat=False)) sleep(0.15) self.assertEqual(1, self.result) def test_error(self): self.jq.put(Job(self.job2, 0.1)) self.jq.put(Job(self.job1, 0.2)) sleep(0.5) self.assertEqual(2, self.result) def test_jobs_tuple(self): self.jq.stop() jobs = tuple(Job(self.job1, t) for t in range(5, 25)) for job in jobs: self.jq.put(job) self.assertTupleEqual(jobs, self.jq.jobs()) def test_inUpdater(self): u = Updater(bot="MockBot") u.job_queue.start() try: u.job_queue.put(Job(self.job1, 0.5)) sleep(0.75) self.assertEqual(1, self.result) u.stop() sleep(2) self.assertEqual(1, self.result) finally: u.stop() def test_time_unit_int(self): # Testing seconds in int delta = 2 expected_time = time.time() + delta self.jq.put(Job(self.job5, delta, repeat=False)) sleep(2.5) self.assertAlmostEqual(self.job_time, expected_time, delta=0.1) def test_time_unit_dt_timedelta(self): # Testing seconds, minutes and hours as datetime.timedelta object # This is sufficient to test that it actually works. interval = datetime.timedelta(seconds=2) expected_time = time.time() + interval.total_seconds() self.jq.put(Job(self.job5, interval, repeat=False)) sleep(2.5) self.assertAlmostEqual(self.job_time, expected_time, delta=0.1) def test_time_unit_dt_datetime(self): # Testing running at a specific datetime delta = datetime.timedelta(seconds=2) next_t = datetime.datetime.now() + delta expected_time = time.time() + delta.total_seconds() self.jq.put(Job(self.job5, repeat=False), next_t=next_t) sleep(2.5) self.assertAlmostEqual(self.job_time, expected_time, delta=0.1) def test_time_unit_dt_time_today(self): # Testing running at a specific time today delta = 2 next_t = (datetime.datetime.now() + datetime.timedelta(seconds=delta)).time() expected_time = time.time() + delta self.jq.put(Job(self.job5, repeat=False), next_t=next_t) sleep(2.5) self.assertAlmostEqual(self.job_time, expected_time, delta=0.1) def test_time_unit_dt_time_tomorrow(self): # Testing running at a specific time that has passed today. Since we can't wait a day, we # test if the jobs next_t has been calculated correctly delta = -2 next_t = (datetime.datetime.now() + datetime.timedelta(seconds=delta)).time() expected_time = time.time() + delta + 60 * 60 * 24 self.jq.put(Job(self.job5, repeat=False), next_t=next_t) self.assertAlmostEqual(self.jq.queue.get(False)[0], expected_time, delta=0.1) def test_run_once(self): delta = 2 expected_time = time.time() + delta self.jq.run_once(self.job5, delta) sleep(2.5) self.assertAlmostEqual(self.job_time, expected_time, delta=0.1) def test_run_repeating(self): interval = 0.1 first = 1.5 self.jq.run_repeating(self.job1, interval, first=first) sleep(2.505) self.assertAlmostEqual(self.result, 10, delta=1) def test_run_daily(self): delta = 1 time_of_day = (datetime.datetime.now() + datetime.timedelta(seconds=delta)).time() expected_time = time.time() + 60 * 60 * 24 + delta self.jq.run_daily(self.job1, time_of_day) sleep(2 * delta) self.assertEqual(self.result, 1) self.assertAlmostEqual(self.jq.queue.get(False)[0], expected_time, delta=0.1)
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] = []
class JobQueueTest(BaseTest, unittest.TestCase): """ This object represents Tests for Updater, Dispatcher, WebhookServer and WebhookHandler """ def setUp(self): self.jq = JobQueue("Bot") self.result = 0 def tearDown(self): if self.jq is not None: self.jq.stop() stop_con_pool() def job1(self, bot, job): self.result += 1 def job2(self, bot, job): raise Exception("Test Error") def job3(self, bot, job): self.result += 1 job.schedule_removal() def job4(self, bot, job): self.result += job.context def test_basic(self): self.jq.put(Job(self.job1, 0.1)) sleep(1.5) self.assertGreaterEqual(self.result, 10) def test_job_with_context(self): self.jq.put(Job(self.job4, 0.1, context=5)) sleep(1.5) self.assertGreaterEqual(self.result, 50) def test_noRepeat(self): self.jq.put(Job(self.job1, 0.1, repeat=False)) sleep(0.5) self.assertEqual(1, self.result) def test_nextT(self): self.jq.put(Job(self.job1, 0.1), next_t=0.5) sleep(0.45) self.assertEqual(0, self.result) sleep(0.1) self.assertEqual(1, self.result) def test_multiple(self): self.jq.put(Job(self.job1, 0.1, repeat=False)) self.jq.put(Job(self.job1, 0.2, repeat=False)) self.jq.put(Job(self.job1, 0.4)) sleep(1) self.assertEqual(4, self.result) def test_disabled(self): j0 = Job(self.job1, 0.1) j1 = Job(self.job1, 0.2) self.jq.put(j0) self.jq.put(Job(self.job1, 0.4)) self.jq.put(j1) j0.enabled = False j1.enabled = False sleep(1) self.assertEqual(2, self.result) def test_schedule_removal(self): j0 = Job(self.job1, 0.1) j1 = Job(self.job1, 0.2) self.jq.put(j0) self.jq.put(Job(self.job1, 0.4)) self.jq.put(j1) j0.schedule_removal() j1.schedule_removal() sleep(1) self.assertEqual(2, self.result) def test_schedule_removal_from_within(self): self.jq.put(Job(self.job1, 0.4)) self.jq.put(Job(self.job3, 0.2)) sleep(1) self.assertEqual(3, self.result) def test_longer_first(self): self.jq.put(Job(self.job1, 0.2, repeat=False)) self.jq.put(Job(self.job1, 0.1, repeat=False)) sleep(0.15) self.assertEqual(1, self.result) def test_error(self): self.jq.put(Job(self.job2, 0.1)) self.jq.put(Job(self.job1, 0.2)) self.jq.start() sleep(0.5) self.assertEqual(2, self.result) def test_jobs_tuple(self): self.jq.stop() jobs = tuple(Job(self.job1, t) for t in range(5, 25)) for job in jobs: self.jq.put(job) self.assertTupleEqual(jobs, self.jq.jobs()) def test_inUpdater(self): u = Updater(bot="MockBot") u.job_queue.put(Job(self.job1, 0.5)) sleep(0.75) self.assertEqual(1, self.result) u.stop() sleep(2) self.assertEqual(1, self.result)
class JobQueueTest(BaseTest, unittest.TestCase): """ This object represents Tests for Updater, Dispatcher, WebhookServer and WebhookHandler """ def setUp(self): self.jq = JobQueue("Bot", tick_interval=0.005) self.result = 0 def tearDown(self): if self.jq is not None: self.jq.stop() def job1(self, bot): self.result += 1 def job2(self, bot): raise Exception("Test Error") def test_legacy_import(self): from telegram import JobQueue as legacyJobQueue ljq = legacyJobQueue("Bot", tick_interval=0.005) self.assertIsInstance(ljq, JobQueue) ljq.stop() def test_basic(self): self.jq.put(self.job1, 0.1) sleep(1.5) self.assertGreaterEqual(self.result, 10) def test_noRepeat(self): self.jq.put(self.job1, 0.1, repeat=False) sleep(0.5) self.assertEqual(1, self.result) def test_nextT(self): self.jq.put(self.job1, 0.1, next_t=0.5) sleep(0.45) self.assertEqual(0, self.result) sleep(0.1) self.assertEqual(1, self.result) def test_multiple(self): self.jq.put(self.job1, 0.1, repeat=False) self.jq.put(self.job1, 0.2, repeat=False) self.jq.put(self.job1, 0.4) sleep(1) self.assertEqual(4, self.result) def test_error(self): self.jq.put(self.job2, 0.1) self.jq.put(self.job1, 0.2) self.jq.start() sleep(0.4) self.assertEqual(1, self.result) def test_inUpdater(self): u = Updater(bot="MockBot", job_queue_tick_interval=0.005) u.job_queue.put(self.job1, 0.5) sleep(0.75) self.assertEqual(1, self.result) u.stop() sleep(2) self.assertEqual(1, self.result)
def callback_query(bot: Bot, update: Update, session, chat_data: dict, job_queue: JobQueue): update_group(update.callback_query.message.chat, session) user = add_user(update.effective_user, session) data = json.loads(update.callback_query.data) if data['t'] == QueryType.GroupList.value: msg = MSG_GROUP_STATUS_CHOOSE_CHAT squads = session.query(Squad).all() inline_keys = [] for squad in squads: inline_keys.append( InlineKeyboardButton(squad.squad_name, callback_data=json.dumps({ 't': QueryType.GroupInfo.value, 'id': squad.chat_id }))) inline_markup = InlineKeyboardMarkup([[key] for key in inline_keys]) bot.editMessageText(msg, update.callback_query.message.chat.id, update.callback_query.message.message_id, reply_markup=inline_markup) elif data['t'] == QueryType.GroupInfo.value: msg, inline_markup = generate_group_info(data['id'], session) bot.editMessageText(msg, update.callback_query.message.chat.id, update.callback_query.message.message_id, reply_markup=inline_markup) elif data['t'] == QueryType.DelAdm.value: admin_user = session.query(User).filter_by(id=data['uid']).first() if admin_user: del_adm(bot, data['gid'], admin_user, session) msg, inline_markup = generate_group_info(data['gid'], session) bot.editMessageText(msg, update.callback_query.message.chat.id, update.callback_query.message.message_id, reply_markup=inline_markup) elif data['t'] == QueryType.Order.value: order_text = chat_data['order'] order_type = chat_data['order_type'] order_pin = chat_data['pin'] if 'pin' in chat_data else True order_btn = chat_data['btn'] if 'btn' in chat_data else True if not data['g']: if order_btn: order = Order() order.text = order_text order.chat_id = data['id'] order.date = datetime.now() msg = send_async(bot, chat_id=order.chat_id, text=MSG_ORDER_CLEARED_BY_HEADER + MSG_EMPTY).result() if msg: order.confirmed_msg = msg.message_id else: order.confirmed_msg = 0 session.add(order) session.commit() markup = generate_ok_markup(order.id, 0) msg = send_order(bot, order.text, order_type, order.chat_id, markup).result().result() else: msg = send_order(bot, order_text, order_type, data['id'], None).result().result() if order_pin and msg: try: bot.request.post( bot.base_url + '/pinChatMessage', { 'chat_id': data['id'], 'message_id': msg.message_id, 'disable_notification': False }) except TelegramError as err: bot.logger.error(err.message) else: group = session.query(OrderGroup).filter_by(id=data['id']).first() for item in group.items: if order_btn: order = Order() order.text = order_text order.chat_id = item.chat_id order.date = datetime.now() msg = send_async(bot, chat_id=order.chat_id, text=MSG_ORDER_CLEARED_BY_HEADER + MSG_EMPTY).result() if msg: order.confirmed_msg = msg.message_id else: order.confirmed_msg = 0 session.add(order) session.commit() markup = generate_ok_markup(order.id, 0) msg = send_order(bot, order.text, order_type, order.chat_id, markup).result().result() else: msg = send_order(bot, order_text, order_type, item.chat_id, None).result().result() if order_pin and msg: try: bot.request.post( bot.base_url + '/pinChatMessage', { 'chat_id': item.chat_id, 'message_id': msg.message_id, 'disable_notification': False }) except TelegramError as err: bot.logger.error(err.message) update.callback_query.answer(text=MSG_ORDER_SENT) elif data['t'] == QueryType.OrderOk.value: order = session.query(Order).filter_by(id=data['id']).first() if order is not None: squad = session.query(Squad).filter_by( chat_id=order.chat_id).first() if squad is not None: squad_member = session.query(SquadMember).filter_by( squad_id=squad.chat_id, user_id=update.callback_query.from_user.id, approved=True).first() if squad_member is not None: order_ok = session.query(OrderCleared).filter_by( order_id=data['id'], user_id=squad_member.user_id).first() if order_ok is None and datetime.now( ) - order.date < timedelta(minutes=10): order_ok = OrderCleared() order_ok.order_id = data['id'] order_ok.user_id = update.callback_query.from_user.id session.add(order_ok) session.commit() if order.confirmed_msg != 0: if order.id not in order_updated or \ datetime.now() - order_updated[order.id] > timedelta(seconds=4): order_updated[order.id] = datetime.now() job_queue.run_once(update_confirmed, 5, order) update.callback_query.answer(text=MSG_ORDER_CLEARED) else: update.callback_query.answer( text=MSG_ORDER_CLEARED_ERROR) else: update.callback_query.answer(text=MSG_ORDER_CLEARED_ERROR) else: order_ok = session.query(OrderCleared).filter_by( order_id=data['id'], user_id=update.callback_query.from_user.id).first() if order_ok is None and datetime.now( ) - order.date < timedelta(minutes=10): order_ok = OrderCleared() order_ok.order_id = data['id'] order_ok.user_id = update.callback_query.from_user.id session.add(order_ok) session.commit() if order.confirmed_msg != 0: if order.id not in order_updated or \ datetime.now() - order_updated[order.id] > timedelta(seconds=4): order_updated[order.id] = datetime.now() job_queue.run_once(update_confirmed, 5, order) update.callback_query.answer(text=MSG_ORDER_CLEARED) else: update.callback_query.answer(text=MSG_ORDER_CLEARED_ERROR) elif data['t'] == QueryType.Orders.value: chat_data['order_wait'] = False if 'txt' in data and len(data['txt']): if data['txt'] == Icons.LES.value: chat_data['order'] = Castle.LES.value elif data['txt'] == Icons.GORY.value: chat_data['order'] = Castle.GORY.value elif data['txt'] == Icons.SEA.value: chat_data['order'] = Castle.SEA.value else: chat_data['order'] = data['txt'] markup = generate_order_chats_markup( session, chat_data['pin'] if 'pin' in chat_data else True, chat_data['btn'] if 'btn' in chat_data else True) bot.editMessageText(MSG_ORDER_SEND_HEADER.format(chat_data['order']), update.callback_query.message.chat.id, update.callback_query.message.message_id, reply_markup=markup) elif data['t'] == QueryType.OrderGroup.value: chat_data['order_wait'] = False if 'txt' in data and len(data['txt']): chat_data['order_type'] = MessageType.TEXT if data['txt'] == Icons.LES.value: chat_data['order'] = Castle.LES.value elif data['txt'] == Icons.GORY.value: chat_data['order'] = Castle.GORY.value elif data['txt'] == Icons.SEA.value: chat_data['order'] = Castle.SEA.value else: chat_data['order'] = data['txt'] admin_user = session.query(Admin).filter( Admin.user_id == update.callback_query.from_user.id).all() markup = generate_order_groups_markup( session, admin_user, chat_data['pin'] if 'pin' in chat_data else True, chat_data['btn'] if 'btn' in chat_data else True) bot.editMessageText(MSG_ORDER_SEND_HEADER.format(chat_data['order']), update.callback_query.message.chat.id, update.callback_query.message.message_id, reply_markup=markup) elif data['t'] == QueryType.OrderGroupManage.value: group = session.query(OrderGroup).filter_by(id=data['id']).first() markup = generate_group_manage(data['id'], session) bot.editMessageText(MSG_ORDER_GROUP_CONFIG_HEADER.format(group.name), update.callback_query.message.chat.id, update.callback_query.message.message_id, reply_markup=markup) elif data['t'] == QueryType.OrderGroupTriggerChat.value: group = session.query(OrderGroup).filter_by(id=data['id']).first() deleted = False for item in group.items: if item.chat_id == data['c']: session.delete(item) session.commit() deleted = True if not deleted: item = OrderGroupItem() item.group_id = group.id item.chat_id = data['c'] session.add(item) session.commit() markup = generate_group_manage(data['id'], session) bot.editMessageText(MSG_ORDER_GROUP_CONFIG_HEADER.format(group.name), update.callback_query.message.chat.id, update.callback_query.message.message_id, reply_markup=markup) elif data['t'] == QueryType.OrderGroupAdd.value: chat_data['wait_group_name'] = True send_async(bot, chat_id=update.callback_query.message.chat.id, text=MSG_ORDER_GROUP_NEW) elif data['t'] == QueryType.OrderGroupDelete.value: group = session.query(OrderGroup).filter_by(id=data['id']).first() session.delete(group) session.commit() bot.editMessageText(MSG_ORDER_GROUP_LIST, update.callback_query.message.chat.id, update.callback_query.message.message_id, reply_markup=generate_groups_manage(session)) elif data['t'] == QueryType.OrderGroupList.value: bot.editMessageText(MSG_ORDER_GROUP_LIST, update.callback_query.message.chat.id, update.callback_query.message.message_id, reply_markup=generate_groups_manage(session)) elif data['t'] == QueryType.ShowEquip.value: user = session.query(User).filter_by(id=data['id']).first() update.callback_query.answer(text=MSG_CLEARED) back = data['b'] if 'b' in data else False bot.editMessageText('{}\n{} {}'.format(user.equip.equip, MSG_LAST_UPDATE, user.equip.date), update.callback_query.message.chat.id, update.callback_query.message.message_id, reply_markup=generate_profile_buttons(user, back)) elif data['t'] == QueryType.ShowStock.value: user = session.query(User).filter_by(id=data['id']).first() update.callback_query.answer(text=MSG_CLEARED) back = data['b'] if 'b' in data else False bot.editMessageText('{}\n{} {}'.format( user.stock.stock, MSG_LAST_UPDATE, user.stock.date.strftime("%Y-%m-%d %H:%M:%S")), update.callback_query.message.chat.id, update.callback_query.message.message_id, reply_markup=generate_profile_buttons(user, back)) elif data['t'] == QueryType.ShowHero.value: user = session.query(User).filter_by(id=data['id']).first() update.callback_query.answer(text=MSG_CLEARED) back = data['b'] if 'b' in data else False bot.editMessageText(fill_char_template(MSG_PROFILE_SHOW_FORMAT, user, user.character), update.callback_query.message.chat.id, update.callback_query.message.message_id, reply_markup=generate_profile_buttons(user, back)) elif data['t'] == QueryType.MemberList.value: squad = session.query(Squad).filter_by(chat_id=data['id']).first() markup = generate_squad_members(squad.members, session) bot.editMessageText(squad.squad_name, update.callback_query.message.chat.id, update.callback_query.message.message_id, reply_markup=markup) elif data['t'] == QueryType.LeaveSquad.value: member = session.query(SquadMember).filter_by( user_id=data['id']).first() leave_squad(bot, user, member, update.effective_message, session) elif data['t'] == QueryType.RequestSquad.value: member = session.query(SquadMember).filter_by( user_id=update.callback_query.from_user.id).first() if member is None: member = SquadMember() member.user_id = update.callback_query.from_user.id member.squad_id = data['id'] session.add(member) session.commit() admins = session.query(Admin).filter_by( admin_group=data['id']).all() usernames = [ '@' + session.query(User).filter_by( id=admin.user_id).first().username for admin in admins ] bot.editMessageText(MSG_SQUAD_REQUESTED.format( member.squad.squad_name, ', '.join(usernames)), update.callback_query.message.chat.id, update.callback_query.message.message_id, parse_mode=ParseMode.HTML) admins = session.query(Admin).filter_by( admin_group=member.squad.chat_id).all() for adm in admins: send_async(bot, chat_id=adm.user_id, text=MSG_SQUAD_REQUEST_NEW) else: markup = generate_leave_squad(user.id) bot.editMessageText(MSG_SQUAD_REQUEST_EXISTS, update.callback_query.message.chat.id, update.callback_query.message.message_id, reply_markup=markup) elif data['t'] == QueryType.RequestSquadAccept.value: member = session.query(SquadMember).filter_by(user_id=data['id'], approved=False).first() if member: member.approved = True session.add(member) session.commit() bot.editMessageText( MSG_SQUAD_REQUEST_ACCEPTED.format('@' + member.user.username), update.callback_query.message.chat.id, update.callback_query.message.message_id) admin = session.query(Admin).filter_by( user_id=member.user_id).all() is_admin = False for _ in admin: is_admin = True break send_async(bot, chat_id=member.user_id, text=MSG_SQUAD_REQUEST_ACCEPTED_ANSWER, reply_markup=generate_user_markup(is_admin)) send_async( bot, chat_id=member.squad_id, text=MSG_SQUAD_REQUEST_ACCEPTED.format('@' + member.user.username)) elif data['t'] == QueryType.RequestSquadDecline.value: member = session.query(SquadMember).filter_by(user_id=data['id'], approved=False).first() if member: bot.editMessageText( MSG_SQUAD_REQUEST_DECLINED.format('@' + member.user.username), update.callback_query.message.chat.id, update.callback_query.message.message_id) session.delete(member) session.commit() send_async(bot, chat_id=member.user_id, text=MSG_SQUAD_REQUEST_DECLINED_ANSWER) elif data['t'] == QueryType.InviteSquadAccept.value: if update.callback_query.from_user.id != data['id']: update.callback_query.answer(text=MSG_GO_AWAY) return member = session.query(SquadMember).filter_by( user_id=data['id']).first() if member is None: member = SquadMember() member.user_id = user.id member.squad_id = update.callback_query.message.chat.id member.approved = True session.add(member) session.commit() bot.editMessageText( MSG_SQUAD_ADD_ACCEPTED.format('@' + user.username), update.callback_query.message.chat.id, update.callback_query.message.message_id) elif data['t'] == QueryType.InviteSquadDecline.value: if update.callback_query.from_user.id != data['id']: update.callback_query.answer(text=MSG_GO_AWAY) return user = session.query(User).filter_by(id=data['id']).first() bot.editMessageText( MSG_SQUAD_REQUEST_DECLINED.format('@' + user.username), update.callback_query.message.chat.id, update.callback_query.message.message_id) elif data['t'] == QueryType.TriggerOrderPin.value: if 'pin' in chat_data: chat_data['pin'] = not chat_data['pin'] else: chat_data['pin'] = False if data['g']: admin_user = session.query(Admin).filter( Admin.user_id == update.callback_query.from_user.id).all() markup = generate_order_groups_markup( session, admin_user, chat_data['pin'] if 'pin' in chat_data else True, chat_data['btn'] if 'btn' in chat_data else True) bot.editMessageText(MSG_ORDER_SEND_HEADER.format( chat_data['order']), update.callback_query.message.chat.id, update.callback_query.message.message_id, reply_markup=markup) else: markup = generate_order_chats_markup( session, chat_data['pin'] if 'pin' in chat_data else True, chat_data['btn'] if 'btn' in chat_data else True) bot.editMessageText(MSG_ORDER_SEND_HEADER.format( chat_data['order']), update.callback_query.message.chat.id, update.callback_query.message.message_id, reply_markup=markup) elif data['t'] == QueryType.TriggerOrderButton.value: if 'btn' in chat_data: chat_data['btn'] = not chat_data['btn'] else: chat_data['btn'] = False if data['g']: admin_user = session.query(Admin).filter( Admin.user_id == update.callback_query.from_user.id).all() markup = generate_order_groups_markup( session, admin_user, chat_data['pin'] if 'pin' in chat_data else True, chat_data['btn'] if 'btn' in chat_data else True) bot.editMessageText(MSG_ORDER_SEND_HEADER.format( chat_data['order']), update.callback_query.message.chat.id, update.callback_query.message.message_id, reply_markup=markup) else: markup = generate_order_chats_markup( session, chat_data['pin'] if 'pin' in chat_data else True, chat_data['btn'] if 'btn' in chat_data else True) bot.editMessageText(MSG_ORDER_SEND_HEADER.format( chat_data['order']), update.callback_query.message.chat.id, update.callback_query.message.message_id, reply_markup=markup) elif data['t'] == QueryType.SquadList.value: admin = session.query(Admin).filter_by( user_id=update.callback_query.from_user.id).all() global_adm = False for adm in admin: if adm.admin_type <= AdminType.FULL.value: global_adm = True break if global_adm: squads = session.query(Squad).all() else: group_ids = [] for adm in admin: group_ids.append(adm.admin_group) squads = session.query(Squad).filter( Squad.chat_id in group_ids).all() markup = generate_squad_list(squads, session) bot.editMessageText(MSG_SQUAD_LIST, update.callback_query.message.chat.id, update.callback_query.message.message_id, reply_markup=markup) elif data['t'] == QueryType.GroupDelete.value: squad = session.query(Squad).filter_by(chat_id=data['gid']).first() if squad is not None: for member in squad.members: session.delete(member) session.delete(squad) session.commit() send_async(bot, chat_id=data['gid'], text=MSG_SQUAD_DELETE) msg = MSG_GROUP_STATUS_CHOOSE_CHAT squads = session.query(Squad).all() inline_keys = [] for squad in squads: inline_keys.append( InlineKeyboardButton(squad.squad_name, callback_data=json.dumps({ 't': QueryType.GroupInfo.value, 'id': squad.chat_id }))) inline_markup = InlineKeyboardMarkup([[key] for key in inline_keys]) bot.editMessageText(msg, update.callback_query.message.chat.id, update.callback_query.message.message_id, reply_markup=inline_markup) elif data['t'] == QueryType.OtherReport.value: squad = session.query(Squad).filter(Squad.chat_id == data['c']).first() time_from = datetime.fromtimestamp(data['ts']) time_to = time_from + timedelta(hours=4) reports = session.query(User, Report) \ .join(SquadMember) \ .outerjoin(Report, and_(User.id == Report.user_id, Report.date > time_from, Report.date < time_to)) \ .filter(SquadMember.squad_id == data['c']).order_by(Report.date.desc()).all() text = '' full_def = 0 full_atk = 0 full_exp = 0 full_gold = 0 full_stock = 0 total_reports = 0 total_members = 0 for user, report in reports: total_members += 1 if report: text += MSG_REPORT_SUMMARY_ROW.format( report.name, user.username, report.attack, report.defence, report.earned_exp, report.earned_gold, report.earned_stock) full_atk += report.attack full_def += report.defence full_exp += report.earned_exp full_gold += report.earned_gold full_stock += report.earned_stock total_reports += 1 else: text += MSG_REPORT_SUMMARY_ROW_EMPTY.format( user.character.name, user.username) text = MSG_REPORT_SUMMARY_HEADER.format( squad.squad_name, time_from.strftime('%d-%m-%Y %H:%M'), total_reports, total_members, full_atk, full_def, full_exp, full_gold, full_stock) + text markup = generate_other_reports(time_from, squad.chat_id) bot.editMessageText(text, update.callback_query.message.chat.id, update.callback_query.message.message_id, parse_mode=ParseMode.HTML, reply_markup=markup) update.callback_query.answer(text=MSG_READY) elif data['t'] == QueryType.GlobalBuildTop.value: update.callback_query.answer(text=MSG_TOP_GENERATING) global_build_top(bot, update) elif data['t'] == QueryType.WeekBuildTop.value: update.callback_query.answer(text=MSG_TOP_GENERATING) week_build_top(bot, update) elif data['t'] == QueryType.SquadGlobalBuildTop.value: update.callback_query.answer(text=MSG_TOP_GENERATING) global_squad_build_top(bot, update) elif data['t'] == QueryType.SquadWeekBuildTop.value: update.callback_query.answer(text=MSG_TOP_GENERATING) week_squad_build_top(bot, update) elif data['t'] == QueryType.BattleGlobalTop.value: update.callback_query.answer(text=MSG_TOP_GENERATING) global_battle_top(bot, update) elif data['t'] == QueryType.BattleWeekTop.value: update.callback_query.answer(text=MSG_TOP_GENERATING) week_battle_top(bot, update) elif data['t'] == QueryType.Yes.value: leave_squad(bot, user, user.member, update.effective_message, session) elif data['t'] == QueryType.No.value: bot.editMessageText(MSG_SQUAD_LEAVE_DECLINE, update.callback_query.message.chat.id, update.callback_query.message.message_id)
async def job_queue(bot, app): jq = JobQueue() jq.set_application(app) await jq.start() yield jq await jq.stop()
from queue import Queue import os from playhouse.db_url import connect import dotenv dotenv.load_dotenv() def config(key: str, default=None): return os.environ.get(key) or os.environ.get(key.upper()) or default # db = SqliteDatabase(config('db', 'database.sqlite')) db = Proxy() db.initialize(connect(config('DATABASE_URL', 'sqlite:///database.sqlite'))) class CustomDispatcher(Dispatcher): def process_update(self, update): with db.atomic() as txn: super().process_update(update) db.close() updater = Updater(dispatcher=CustomDispatcher(Bot(config('token')), Queue(), job_queue=JobQueue()), workers=None) bot = updater.bot
class UserCommandHandler(PicBotRoutines): """docstring for UserCommandHandler""" def _command_method(func): """Decorator for functions that are invoked on commands. Ensures that the user is initialized.""" # @functools.wraps(func) def wrapper(self, bot, update, *args, **kwargs): # print("command method", func.__name__, ) # debug # print("self",self)# debug # print("command method", self, bot, update, args, kwargs, sep="||") # debug chat_id = update.message.chat_id log.info("Command method called!", func.__name__, "Chat_id: ", chat_id) # Initialize user, if not present in DB self.database_handler.initializeUser(chat_id=chat_id) log.debug("User initialized") lS = LanguageSupport(self.database_handler.getLang(chat_id)).languageSupport # noinspection PyCallingNonCallable func(self, bot, update, lS) log.debug("Function completed") return wrapper def __init__(self, bot, dispatcher, database_handler, token): self.dispatcher = dispatcher # queue for async jobs self.job_queue = JobQueue(bot) # Where to get pictures from: local filesystem(local) or Dropbox storage (DB) self.pic_source = sr["pic_source"] self.database_handler = database_handler super(UserCommandHandler, self).__init__(token, self.database_handler) self._addHandlers() if self.pic_source == "DB": self.DB_file_updater_thread = None # a thread that updates files self.dropbox_handler = DropboxHandler(self.database_handler) self._updateDBFiles() elif self.pic_source == "local": self.local_cleaner_job = None self._startLocalCleanerJob() self._initializeSubscriptionJobs() def _initializeSubscriptionJobs(self): for chat_id in self.database_handler.getAllSubscribedUserIDs(): log.debug("_initializeSubscriptionJobs chat_id", chat_id) self.createPeriodicSenderTask(chat_id) def _updateDBFiles(self, bot=None, job=None): if not self.DB_file_updater_thread or not self.DB_file_updater_thread.is_alive(): self.DB_file_updater_thread = self.dropbox_handler.updateFiles() job = Job(self._updateDBFiles, interval=sr['file_update_period'], repeat=False) else: log.warning("The Dropbox updater thread hasn't finished yet. Consider increasing FILE_UPDATE_PERIOD in settings!") job = Job(self._updateDBFiles, interval=10, repeat=False) # create periodic job self.job_queue.put(job) def _startLocalCleanerJob(self): """ Creates a delayed async job that cleans database every now and then if local files get deeleted :return: """ log.debug("_startLocalCleanerJob") self.local_cleaner_job = job = Job(self._localCleanerThread, interval=LOCAL_CLEANER_PERIOD, repeat=True) self.job_queue.put(job) def _localCleanerThread(self, bot, job): log.debug("_localCleanerThread") local_files = self.getLocalFiles() bd_files = set(self.database_handler.getFileList()) to_delete = bd_files.difference(local_files) log.debug("to_delete", to_delete) if to_delete: self.database_handler.batchDeleteFiles(to_delete) def _addHandlers(self): self.dispatcher.add_handler(CommandHandler('start', self.command_start)) self.dispatcher.add_handler(CommandHandler('help', self.command_help)) self.dispatcher.add_handler(CommandHandler('about', self.command_about)) self.dispatcher.add_handler(CommandHandler('otherbots', self.command_otherbots)) self.dispatcher.add_handler(CommandHandler('gimmepic', self.command_gimmepic)) self.dispatcher.add_handler(CommandHandler('subscribe', self.command_subscribe)) self.dispatcher.add_handler(CommandHandler('unsubscribe', self.command_unsubscribe)) self.dispatcher.add_handler(CommandHandler('spamuncached', self.command_spamuncached)) # non-command message self.dispatcher.add_handler(MessageHandler([Filters.text], self.messageMethod)) # unknown commands self.dispatcher.add_handler(MessageHandler([Filters.command], self.unknown_command)) self.dispatcher.add_error_handler(self.error_handler) log.info("Commands set!") def setPeriod(self, bot, update, lS=None): message = update.message.text chat_id = update.message.chat_id try: new_period = int(message) if not self.database_handler.getSubscribed(chat_id): self.sendMessageCommandMethod(bot, update, "You're not subscribed yet! /subscribe first!") else: # If a period is too small if new_period < MIN_PICTURE_SEND_PERIOD: self.database_handler.setPeriod(chat_id, MIN_PICTURE_SEND_PERIOD) self.sendMessageCommandMethod(bot, update, "The minimum possible period is {0}.\nSetting period to {0}.".format( str(MIN_PICTURE_SEND_PERIOD))) # If a period is too big elif new_period > MAX_PICTURE_SEND_PERIOD: self.database_handler.setPeriod(chat_id, MAX_PICTURE_SEND_PERIOD) self.sendMessageCommandMethod(bot, update, "The maximum possible period is {0}.\nSetting period to {0}.".format( str(MAX_PICTURE_SEND_PERIOD))) # If a period length is fine - accept else: self.database_handler.setPeriod(chat_id, new_period) self.sendMessageCommandMethod(bot, update, "Setting period to {0}.".format(new_period) ) # Reset timer self.database_handler.resetTimer(chat_id) self.restartPeriodicTask(chat_id) return True # user has sent a bullsh*t command except ValueError: return False def doGimmepic(self, chat_id): if self.pic_source == "local": self.sendLocalRandomPic(chat_id) elif self.pic_source == "DB": self.sendDropboxRandomPic(chat_id) def _periodicSender(self, bot, job): chat_id = job.context self.doGimmepic(chat_id) self.database_handler.resetTimer(chat_id) self.restartPeriodicTask(chat_id) def restartPeriodicTask(self, chat_id): self.removePeriodicSenderTask(chat_id) self.createPeriodicSenderTask(chat_id) def createPeriodicSenderTask(self, chat_id): time_left = self.database_handler.getSendTime(chat_id) - time() log.debug("Time left:", time_left) job = Job(self._periodicSender, time_left, context=chat_id) subscriptions_tasks[chat_id] = job self.job_queue.put(job) def removePeriodicSenderTask(self, chat_id): subscriptions_tasks[chat_id].schedule_removal() # remove task from job queue del subscriptions_tasks[chat_id] ########## # COMMAND METHODS ########## # GENERIC COMMANDS # noinspection PyArgumentList @_command_method def command_start(self, bot, update, lS=None): self.sendMessageCommandMethod(bot, update, lS(START_MESSAGE)) # noinspection PyArgumentList @_command_method def command_help(self, bot, update, lS=None): msg = lS(HELP_MESSAGE).format(sr['picture_send_period'],MIN_PICTURE_SEND_PERIOD, MAX_PICTURE_SEND_PERIOD) self.sendMessageCommandMethod(bot, update, msg) # noinspection PyArgumentList @_command_method def command_about(self, bot, update, lS=None): msg = lS(ABOUT_MESSAGE).format(".".join([str(i) for i in VERSION_NUMBER])) self.sendMessageCommandMethod(bot, update, msg, disable_web_page_preview=False) # noinspection PyArgumentList # @_command_method def command_otherbots(self, bot, update, lS=None): # a = 2/0 self.sendMessageCommandMethod(bot, update, OTHER_BOTS_MESSAGE) # noinspection PyArgumentList @_command_method def messageMethod(self, bot, update, lS=None): chat_id = update.message.chat_id message = update.message.text log.info("messageMethod. Chat_id:", chat_id, "Message:", message) if message in LanguageSupport.allVariants(HELP_BUTTON): self.command_help(bot, update, lS) elif message in LanguageSupport.allVariants(ABOUT_BUTTON): self.command_about(bot, update, lS) elif message in LanguageSupport.allVariants(OTHER_BOTS_BUTTON): self.command_otherbots(bot, update, lS) # elif message == EN_LANG_BUTTON: # self.command_set_lang_en(bot, update, lS) # elif message == RU_LANG_BUTTON: # self.command_set_lang_ru(bot, update, lS) elif message in LanguageSupport.allVariants(GIMMEPIC_BUTTON): self.command_gimmepic(bot, update, lS) elif message in LanguageSupport.allVariants(SUBSCRIBE_BUTTON): self.command_subscribe(bot, update, lS) elif message in LanguageSupport.allVariants(UNSUBSCRIBE_BUTTON): self.command_unsubscribe(bot, update, lS) elif message in LanguageSupport.allVariants(SHOW_PERIOD_BUTTON): self.command_show_period(bot, update, lS) else: if not self.setPeriod(bot, update, lS): self.unknown_command(bot, update, lS) # noinspection PyArgumentList @_command_method def unknown_command(self, bot, update, lS=None): self.sendMessageCommandMethod(bot, update, UNKNOWN_COMMAND_MESSAGE) def error_handler(self, bot, update, error): print(error) # PICBOT COMMANDS # noinspection PyArgumentList @_command_method def command_gimmepic(self, bot, update, lS=None): chat_id = update.message.chat_id self.doGimmepic(chat_id) # noinspection PyArgumentList @_command_method def command_subscribe(self, bot, update, lS=None): chat_id = update.message.chat_id period = self.database_handler.getPeriod(chat_id) if self.database_handler.getSubscribed(chat_id): self.sendMessageCommandMethod(bot, update, lS(ALREADY_SUBSCRIBED_MESSAGE).format(period)) else: self.database_handler.subscribeUser(chat_id) self.database_handler.resetTimer(chat_id) self.createPeriodicSenderTask(chat_id) self.sendMessageCommandMethod(bot, update, lS(SUBSCRIBED_MESSAGE).format(period)) # noinspection PyArgumentList @_command_method def command_unsubscribe(self, bot, update, lS=None): chat_id = update.message.chat_id if not self.database_handler.getSubscribed(chat_id): self.sendMessageCommandMethod(bot, update, lS(NOT_SUBSCRIBED_YET_MESSAGE)) else: self.database_handler.unsubscribeUser(chat_id) self.removePeriodicSenderTask(chat_id) self.sendMessageCommandMethod(bot, update, lS(UNSUBSCRIBED_MESSAGE)) # noinspection PyArgumentList @_command_method def command_show_period(self, bot, update, lS=None): chat_id = update.message.chat_id period = self.database_handler.getPeriod(chat_id) self.sendMessageCommandMethod(bot, update, """An image is sent to you every {0} seconds.""".format(period)) # noinspection PyArgumentList @_command_method def command_spamuncached(self, bot, update, lS=None): chat_id = update.message.chat_id self.sendUncachedImages(chat_id, self.pic_source)
def setUp(self): self.jq = JobQueue("Bot", tick_interval=0.005) self.result = 0
class Updater(object): """ This class, which employs the Dispatcher class, provides a frontend to 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: Args: token (Optional[str]): The bot's token given by the @BotFather base_url (Optional[str]): workers (Optional[int]): Amount of threads in the thread pool for functions decorated with @run_async bot (Optional[Bot]): job_queue_tick_interval(Optional[float]): The interval the queue should be checked for new tasks. Defaults to 1.0 Raises: ValueError: If both `token` and `bot` are passed or none of them. """ def __init__(self, token=None, base_url=None, workers=4, bot=None, job_queue_tick_interval=1.0): 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 bot is not None: self.bot = bot else: self.bot = Bot(token, base_url) self.update_queue = Queue() self.job_queue = JobQueue(self.bot, job_queue_tick_interval) self.__exception_event = Event() self.dispatcher = Dispatcher(self.bot, self.update_queue, workers, self.__exception_event) self.last_update_id = 0 self.logger = logging.getLogger(__name__) self.running = False self.is_idle = False self.httpd = None self.__lock = Lock() self.__threads = [] """:type: list[Thread]""" 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') raise self.logger.debug('{0} - ended'.format(thr_name)) def start_polling(self, poll_interval=0.0, timeout=10, network_delay=2, clean=False, bootstrap_retries=0): """ Starts polling updates from Telegram. Args: poll_interval (Optional[float]): Time to wait between polling updates from Telegram in seconds. Default is 0.0 timeout (Optional[float]): Passed to Bot.getUpdates network_delay (Optional[float]): Passed to Bot.getUpdates clean (Optional[bool]): Whether to clean any pending updates on Telegram servers before actually starting to poll. Default is False. bootstrap_retries (Optional[int[): Whether the bootstrapping phase of the `Updater` will retry on failures on the Telegram server. | < 0 - retry indefinitely | 0 - no retries (default) | > 0 - retry up to X times Returns: 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._init_thread(self.dispatcher.start, "dispatcher") self._init_thread(self._start_polling, "updater", poll_interval, timeout, network_delay, bootstrap_retries, clean) # 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): """ 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 (Optional[str]): IP-Address to listen on port (Optional[int]): Port the bot should be listening on url_path (Optional[str]): Path inside url cert (Optional[str]): Path to the SSL certificate file key (Optional[str]): Path to the SSL key file clean (Optional[bool]): Whether to clean any pending updates on Telegram servers before actually starting the webhook. Default is False. bootstrap_retries (Optional[int[): Whether the bootstrapping phase of the `Updater` will retry on failures on the Telegram server. | < 0 - retry indefinitely | 0 - no retries (default) | > 0 - retry up to X times webhook_url (Optional[str]): Explicitly specifiy the webhook url. Useful behind NAT, reverse proxy, etc. Default is derived from `listen`, `port` & `url_path`. Returns: 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._init_thread(self.dispatcher.start, "dispatcher"), self._init_thread(self._start_webhook, "updater", listen, port, url_path, cert, key, bootstrap_retries, clean, webhook_url) # Return the update queue so the main thread can insert updates return self.update_queue def _start_polling(self, poll_interval, timeout, network_delay, bootstrap_retries, clean): """ Thread target of thread 'updater'. Runs in background, pulls updates from Telegram and inserts them in the update queue of the Dispatcher. """ cur_interval = poll_interval self.logger.debug('Updater thread started') self._bootstrap(bootstrap_retries, clean=clean, webhook_url='') while self.running: try: updates = self.bot.getUpdates(self.last_update_id, timeout=timeout, network_delay=network_delay) except TelegramError as te: self.logger.error("Error while getting Updates: {0}".format(te)) # Put the error into the update queue and let the Dispatcher # broadcast it self.update_queue.put(te) cur_interval = self._increase_poll_interval(cur_interval) else: if not self.running: if len(updates) > 0: self.logger.debug('Updates ignored and will be pulled ' 'again on restart.') break if updates: for update in updates: self.update_queue.put(update) self.last_update_id = updates[-1].update_id + 1 cur_interval = poll_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): self.logger.debug('Updater thread started') 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) 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')) 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, cert=None): retries = 0 while True: try: if clean: # Disable webhook for cleaning self.bot.setWebhook(webhook_url='') self._clean_updates() self.bot.setWebhook(webhook_url=webhook_url, certificate=cert) except (Unauthorized, InvalidToken): raise except TelegramError: msg = 'error in bootstrap phase; try={0} max_retries={1}'\ .format(retries, max_retries) if max_retries < 0 or retries < max_retries: self.logger.warning(msg) retries += 1 else: self.logger.exception(msg) raise else: break sleep(1) def _clean_updates(self): self.logger.debug('Cleaning updates from Telegram server') updates = self.bot.getUpdates() while updates: updates = self.bot.getUpdates(updates[-1].update_id + 1) def stop(self): """ Stops the polling/webhook thread, the dispatcher and the job queue """ self.job_queue.stop() with self.__lock: if self.running: self.logger.debug('Stopping Updater and Dispatcher...') self.running = False self._stop_httpd() self._stop_dispatcher() self._join_threads() # async threads must be join()ed only after the dispatcher # thread was joined, otherwise we can still have new async # threads dispatched self._join_async_threads() 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_async_threads(self): with dispatcher.async_lock: threads = list(dispatcher.async_threads) total = len(threads) for i, thr in enumerate(threads): self.logger.debug('Waiting for async thread {0}/{1} to end'.format(i, total)) thr.join() self.logger.debug('async thread {0}/{1} has ended'.format(i, total)) 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.stop() 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: 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)
def setUp(self): self.jq = JobQueue("Bot") self.result = 0
def new_member(bot: Bot, update: Update, job_queue: JobQueue): chat = update.effective_chat user = update.effective_user msg = update.effective_message should_welc, cust_welcome, welc_type = sql.get_welc_pref(chat.id) welc_mutes = sql.welcome_mutes(chat.id) human_checks = sql.get_human_checks(user.id, chat.id) new_members = update.effective_message.new_chat_members for new_mem in new_members: welcome_log = None sent = None should_mute = True welcome_bool = True if should_welc: # Give the owner a special welcome if new_mem.id == OWNER_ID: update.effective_message.reply_text("Hi, Selamat Bergabung.") welcome_log = (f"{html.escape(chat.title)}\n" f"#NEWMEM\n" f"Pemilik bot bergabung ke grup ini") # Welcome Devs elif new_mem.id in DEV_USERS: update.effective_message.reply_text( "Whoa! A member of the Heroes Association just joined!") # Welcome Sudos elif new_mem.id in SUDO_USERS: update.effective_message.reply_text( "Huh! A Dragon disaster just joined! Stay Alert!") # Welcome Support elif new_mem.id in SUPPORT_USERS: update.effective_message.reply_text( "Huh! Someone with a Demon disaster level just joined!") # Welcome Whitelisted elif new_mem.id in TIGER_USERS: update.effective_message.reply_text( "Oof! A Tiger disaster just joined!") # Welcome Tigers elif new_mem.id in WHITELIST_USERS: update.effective_message.reply_text( "Oof! A Wolf disaster just joined!") # Welcome yourself elif new_mem.id == bot.id: update.effective_message.reply_text("hello 😎") else: # If welcome message is media, send with appropriate function if welc_type not in (sql.Types.TEXT, sql.Types.BUTTON_TEXT): ENUM_FUNC_MAP[welc_type](chat.id, cust_welcome) continue # else, move on first_name = new_mem.first_name or "PersonWithNoName" # edge case of empty name - occurs for some bugs. if cust_welcome: if cust_welcome == sql.DEFAULT_WELCOME: cust_welcome = random.choice( sql.DEFAULT_WELCOME_MESSAGES).format( first=escape_markdown(first_name)) if new_mem.last_name: fullname = escape_markdown( f"{first_name} {new_mem.last_name}") else: fullname = escape_markdown(first_name) count = chat.get_members_count() mention = mention_markdown(new_mem.id, escape_markdown(first_name)) if new_mem.username: username = "******" + escape_markdown(new_mem.username) else: username = mention valid_format = escape_invalid_curly_brackets( cust_welcome, VALID_WELCOME_FORMATTERS) res = valid_format.format( first=escape_markdown(first_name), last=escape_markdown(new_mem.last_name or first_name), fullname=escape_markdown(fullname), username=username, mention=mention, count=count, chatname=escape_markdown(chat.title), id=new_mem.id) buttons = sql.get_welc_buttons(chat.id) keyb = build_keyboard(buttons) else: res = random.choice(sql.DEFAULT_WELCOME_MESSAGES).format( first=escape_markdown(first_name)) keyb = [] backup_message = random.choice( sql.DEFAULT_WELCOME_MESSAGES).format( first=escape_markdown(first_name)) keyboard = InlineKeyboardMarkup(keyb) else: welcome_bool = False res = None keyboard = None backup_message = None # User exceptions from welcomemutes if is_user_ban_protected(chat, new_mem.id, chat.get_member( new_mem.id)) or human_checks: should_mute = False # Join welcome: soft mute if new_mem.is_bot: should_mute = False if user.id == new_mem.id: if should_mute: if welc_mutes == "soft": bot.restrict_chat_member(chat.id, new_mem.id, can_send_messages=True, can_send_media_messages=False, can_send_other_messages=False, can_add_web_page_previews=False, until_date=(int(time.time() + 24 * 60 * 60))) if welc_mutes == "strong": welcome_bool = False VERIFIED_USER_WAITLIST.update({ new_mem.id: { "should_welc": should_welc, "status": False, "update": update, "res": res, "keyboard": keyboard, "backup_message": backup_message } }) new_join_mem = f"[{escape_markdown(new_mem.first_name)}](tg://user?id={user.id})" message = msg.reply_text( f"{new_join_mem}, Klik tombol dibawah untuk verifikasi jika anda ingin bergabung ke grup ini.\nAnda mempunyai waktu 160 detik untuk verifikasi.", reply_markup=InlineKeyboardMarkup([{ InlineKeyboardButton( text="Saya Ingin Bergabung", callback_data=f"user_join_({new_mem.id})") }]), parse_mode=ParseMode.MARKDOWN) bot.restrict_chat_member(chat.id, new_mem.id, can_send_messages=False, can_send_media_messages=False, can_send_other_messages=False, can_add_web_page_previews=False) job_queue.run_once(partial(check_not_bot, new_mem, chat.id, message.message_id), 160, name="welcomemute") if welcome_bool: sent = send(update, res, keyboard, backup_message) prev_welc = sql.get_clean_pref(chat.id) if prev_welc: try: bot.delete_message(chat.id, prev_welc) except BadRequest: pass if sent: sql.set_clean_welcome(chat.id, sent.message_id) if welcome_log: return welcome_log return (f"{html.escape(chat.title)}\n" f"#NEWMEM\n" f"<b>Pengguna</b>: {mention_html(user.id, user.first_name)}\n" f"<b>ID</b>: <code>{user.id}</code>") return ""
def edit_message_photo(job_queue: JobQueue, chat_id, message_id, media_id): job_queue.run_once( lambda _: edit_message_photo_task(chat_id, message_id, media_id), 0)
class Updater(object): """ This class, which employs the :class:`telegram.ext.Dispatcher`, provides a frontend to :class:`telegram.Bot` to the programmer, so they can focus on coding the bot. Its purpose is to receive the updates from Telegram and to deliver them to said dispatcher. It also runs in a separate thread, so the user can interact with the bot, for example on the command line. The dispatcher supports handlers for different kinds of data: Updates from Telegram, basic text commands and even arbitrary types. The updater can be started as a polling service or, for production, use a webhook to receive updates. This is achieved using the WebhookServer and WebhookHandler classes. Attributes: bot (:class:`telegram.Bot`): The bot used with this Updater. user_sig_handler (:obj:`signal`): signals the updater will respond to. update_queue (:obj:`Queue`): Queue for the updates. job_queue (:class:`telegram.ext.JobQueue`): Jobqueue for the updater. dispatcher (:class:`telegram.ext.Dispatcher`): Dispatcher that handles the updates and dispatches them to the handlers. running (:obj:`bool`): Indicates if the updater is running. 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 request object (ignored if `bot` argument is used). 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): 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.bot) self.__exception_event = Event() self.dispatcher = Dispatcher( self.bot, self.update_queue, job_queue=self.job_queue, workers=workers, exception_event=self.__exception_event) 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') raise self.logger.debug('{0} - ended'.format(thr_name)) def start_polling(self, poll_interval=0.0, timeout=10, network_delay=None, clean=False, bootstrap_retries=0, 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 * 0 - no retries (default) * > 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). network_delay: Deprecated. Will be honoured as :attr:`read_latency` for a while but will be removed in the future. Returns: :obj:`Queue`: The update queue that can be filled from the main thread. """ if network_delay is not None: warnings.warn('network_delay is deprecated, use read_latency instead') read_latency = network_delay 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_polling, "updater", poll_interval, timeout, read_latency, bootstrap_retries, clean, allowed_updates) # 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 * 0 - no retries (default) * > 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): # """ # Thread target of thread 'updater'. Runs in background, pulls # updates from Telegram and inserts them in the update queue of the # Dispatcher. # """ cur_interval = poll_interval self.logger.debug('Updater thread started') self._bootstrap(bootstrap_retries, clean=clean, webhook_url='', allowed_updates=None) while self.running: try: updates = self.bot.get_updates( self.last_update_id, timeout=timeout, read_latency=read_latency, allowed_updates=allowed_updates) except RetryAfter as e: self.logger.info(str(e)) cur_interval = 0.5 + e.retry_after except TelegramError as te: self.logger.error("Error while getting Updates: {0}".format(te)) # Put the error into the update queue and let the Dispatcher # broadcast it self.update_queue.put(te) cur_interval = self._increase_poll_interval(cur_interval) else: if not self.running: if len(updates) > 0: self.logger.debug('Updates ignored and will be pulled ' 'again on restart.') break if updates: for update in updates: self.update_queue.put(update) self.last_update_id = updates[-1].update_id + 1 cur_interval = poll_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') 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): retries = 0 while 1: try: if clean: # Disable webhook for cleaning self.bot.delete_webhook() self._clean_updates() sleep(1) self.bot.set_webhook( url=webhook_url, certificate=cert, allowed_updates=allowed_updates) except (Unauthorized, InvalidToken): raise except TelegramError: msg = 'error in bootstrap phase; try={0} max_retries={1}'.format(retries, max_retries) if max_retries < 0 or retries < max_retries: self.logger.warning(msg) retries += 1 else: self.logger.exception(msg) raise else: break sleep(1) def _clean_updates(self): 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) 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.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)
def after_edit_delete_previous_message(job_queue: JobQueue, log_id): job_queue.run_once( lambda _: after_edit_delete_previous_message_task(log_id), 0)
def set_photo(job_queue: JobQueue, log_id, file_id): job_queue.run_once(lambda _: set_photo_task(log_id, file_id), 0)
def _remove_message_after(message: Message, job_queue: JobQueue, seconds: int): logger.debug(f"Scheduling cleanup of message {message} \ in {seconds} seconds") job_queue.run_once(lambda _: message.delete(), seconds, context=message.chat_id)
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: """ 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` Use the context based callback API (ignored if :attr:`dispatcher` argument is used). During the deprecation period of the old API the default is :obj:`False`. **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=None, base_url=None, workers=4, bot=None, private_key=None, private_key_password=None, user_sig_handler=None, request_kwargs=None, persistence=None, defaults=None, use_context=False, dispatcher=None, base_file_url=None): 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, 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() 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 = [] # Just for passing to WebhookAppClass self._default_quote = defaults.quote if defaults else None def _init_thread(self, target, name, *args, **kwargs): 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, *args, **kwargs): thr_name = current_thread().name self.logger.debug('{} - 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('{} - 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 :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() 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 :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`. 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 :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 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 = '/{}'.format(url_path) # Create Tornado app instance app = WebhookAppClass(url_path, self.bot, self.update_queue, default_quote=self._default_quote) # 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: raise TelegramError('Invalid SSL Certificate') 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() @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 {} thread to end'.format(thr.name)) thr.join() self.logger.debug('{} 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))) 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!') 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:`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)
from db import (Bounter, db_commit, db) from googlesheet.gs_updater import UpdateSheet CHANNEL_MONITOR = '@dalematbounty' #BOUNTY_GROUP_ID = -1001243665227 BOUNTY_GROUP_ID = -1001131305306 ADMIN_ID = 171671406 BOT = bot_instance() update_queue, dispatcher = Setup(BOT) logging.basicConfig( format='%(asctime)s - %(name)s - %(levelname)s - %(message)s', level=logging.INFO) job_queue = JobQueue(BOT) job_queue.start() dispatcher.job_queue = job_queue #google_sheet = UpdateSheet('client_secret.json', 'Dalecoin.org', 'Dalc Bounty List') google_sheet = UpdateSheet('client_secret.json', 'Dalecoin Bounty', 'telegram') #=========BOT commands============================== def stop_and_restart(): """Gracefully stop the Updater and replace the current process with a new one""" job_queue.stop() os.execl(sys.executable, sys.executable, *sys.argv)
def test_bot_in_init_deprecation(self, bot): with pytest.warns(TelegramDeprecationWarning): JobQueue(bot)
def stop_aoc_handlers(queue: JobQueue, bot: Bot): (job, ) = queue.get_jobs_by_name(JOB_AOC_UPDATE) if job is not None: job.schedule_removal()
def jq_add_festive_stats(jq: JobQueue): jq.run_repeating(stat_unprocessed, interval=STAT_UNPROCESSED_INTERVAL, first=1)
def question_handler(bot: Bot, update: Update, user_map: DataSet, job_queue: JobQueue): try: # Get the user from the dict and its question_set (by language) user = user_map.participants[update.message.chat_id] # type: Participant # Case for very first question. if user.question_ == -1: user.set_active(True) user.set_language(update.message.text) user.set_block(0) q_set = user_map.return_question_set_by_language(user.language_) user.q_set_ = q_set current_day = q_set[0]["day"] user.set_day(current_day) user.set_block(0) elif user.q_idle_: q_set = user.q_set_ # Get the matching question for the users answer. pointer = user.pointer_ d_prev = q_set[pointer] b_prev = d_prev["blocks"][user.block_] q_prev = b_prev["questions"][user.question_] if not valid_answer(q_prev, update.message.text, user): user.set_q_idle(True) return # Storing the answer and moving on the next question store_answer(user, update.message.text, q_prev, job_queue) user.set_q_idle(False) else: # User has send something without being asked a question. return except KeyError as error: print(error) return if not user.active_: return message, question = find_next_question(user) if question is not None: message = question["text"] q_keyboard = get_keyboard(question["choice"], user) try: bot.send_message(chat_id=user.chat_id_, text=message, reply_markup=q_keyboard) debug(flag="MSG", text=str(user.chat_id_) + ": " + message + "\n") except TelegramError as error: if error.message == 'Unauthorized': user.pause() user.set_q_idle(True) elif user.auto_queue_ is False: user.block_complete_ = True next_day = user.set_next_block() if next_day is None: finished(user, job_queue) return element = user.next_block[2] day_offset = next_day - user.day_ time_t = calc_block_time(element["time"]) due = calc_delta_t(time_t, day_offset, user.timezone_) debug('QUEUE', 'next block in ' + str(due) + ' seconds. User: ' + str(user.chat_id_), log=True) new_job = Job(queue_next, due, repeat=False, context=[user, job_queue]) user.job_ = new_job job_queue.put(new_job)
def new_member(bot: Bot, update: Update, job_queue: JobQueue): chat = update.effective_chat user = update.effective_user msg = update.effective_message should_welc, cust_welcome, welc_type = sql.get_welc_pref(chat.id) welc_mutes = sql.welcome_mutes(chat.id) human_checks = sql.get_human_checks(user.id, chat.id) new_members = update.effective_message.new_chat_members for new_mem in new_members: welcome_log = None sent = None should_mute = True welcome_bool = True if should_welc: # Give the owner a special welcome if new_mem.id == OWNER_ID: update.effective_message.reply_text( "Oh, Genos? Let's get this moving.") welcome_log = (f"{html.escape(chat.title)}\n" f"#USER_JOINED\n" f"Bot Owner just joined the chat") # Welcome Devs elif new_mem.id in DEV_USERS: update.effective_message.reply_text( "Whoa! A member of the Eagle Union just joined!") # Welcome Sudos elif new_mem.id in SUDO_USERS: update.effective_message.reply_text( "Huh! A Royal Nation just joined! Stay Alert!") # Welcome Support elif new_mem.id in SUPPORT_USERS: update.effective_message.reply_text( "Huh! Someone with a Sakura Nation level just joined!") # Welcome Whitelisted elif new_mem.id in SARDEGNA_USERS: update.effective_message.reply_text( "Oof! A Sardegna Nation just joined!") # Welcome Sardegnas elif new_mem.id in WHITELIST_USERS: update.effective_message.reply_text( "Oof! A Neptunia Nation just joined!") # Welcome yourself elif new_mem.id == bot.id: update.effective_message.reply_text("Watashi ga kitta!") else: # If welcome message is media, send with appropriate function if welc_type not in (sql.Types.TEXT, sql.Types.BUTTON_TEXT): ENUM_FUNC_MAP[welc_type](chat.id, cust_welcome) continue # else, move on # edge case of empty name - occurs for some bugs. first_name = new_mem.first_name or "PersonWithNoName" if cust_welcome: if cust_welcome == sql.DEFAULT_WELCOME: cust_welcome = random.choice( sql.DEFAULT_WELCOME_MESSAGES).format( first=escape_markdown(first_name)) if new_mem.last_name: fullname = escape_markdown( f"{first_name} {new_mem.last_name}") else: fullname = escape_markdown(first_name) count = chat.get_members_count() mention = mention_markdown(new_mem.id, escape_markdown(first_name)) if new_mem.username: username = "******" + escape_markdown(new_mem.username) else: username = mention valid_format = escape_invalid_curly_brackets( cust_welcome, VALID_WELCOME_FORMATTERS) res = valid_format.format( first=escape_markdown(first_name), last=escape_markdown(new_mem.last_name or first_name), fullname=escape_markdown(fullname), username=username, mention=mention, count=count, chatname=escape_markdown(chat.title), id=new_mem.id) buttons = sql.get_welc_buttons(chat.id) keyb = build_keyboard(buttons) else: res = random.choice(sql.DEFAULT_WELCOME_MESSAGES).format( first=escape_markdown(first_name)) keyb = [] backup_message = random.choice( sql.DEFAULT_WELCOME_MESSAGES).format( first=escape_markdown(first_name)) keyboard = InlineKeyboardMarkup(keyb) else: welcome_bool = False res = None keyboard = None backup_message = None # User exceptions from welcomemutes if is_user_ban_protected(chat, new_mem.id, chat.get_member( new_mem.id)) or human_checks: should_mute = False # Join welcome: soft mute if new_mem.is_bot: should_mute = False if user.id == new_mem.id: if should_mute: if welc_mutes == "soft": bot.restrict_chat_member(chat.id, new_mem.id, can_send_messages=True, can_send_media_messages=False, can_send_other_messages=False, can_add_web_page_previews=False, until_date=(int(time.time() + 24 * 60 * 60))) if welc_mutes == "strong": welcome_bool = False VERIFIED_USER_WAITLIST.update({ new_mem.id: { "should_welc": should_welc, "status": False, "update": update, "res": res, "keyboard": keyboard, "backup_message": backup_message } }) new_join_mem = f"[{escape_markdown(new_mem.first_name)}](tg://user?id={user.id})" message = msg.reply_text( f"{new_join_mem}, click the button below to prove you're human.\nYou have 160 seconds.", reply_markup=InlineKeyboardMarkup([{ InlineKeyboardButton( text="Yes, I'm human.", callback_data=f"user_join_({new_mem.id})") }]), parse_mode=ParseMode.MARKDOWN) bot.restrict_chat_member(chat.id, new_mem.id, can_send_messages=False, can_send_media_messages=False, can_send_other_messages=False, can_add_web_page_previews=False) job_queue.run_once(partial(check_not_bot, new_mem, chat.id, message.message_id), 160, name="welcomemute") if welcome_bool: sent = send(update, res, keyboard, backup_message) prev_welc = sql.get_clean_pref(chat.id) if prev_welc: try: bot.delete_message(chat.id, prev_welc) except BadRequest: pass if sent: sql.set_clean_welcome(chat.id, sent.message_id) if welcome_log: return welcome_log return (f"{html.escape(chat.title)}\n" f"#USER_JOINED\n" f"<b>User</b>: {mention_html(user.id, user.first_name)}\n" f"<b>ID</b>: <code>{user.id}</code>") return ""
def new_member(bot: Bot, update: Update, job_queue: JobQueue): chat = update.effective_chat user = update.effective_user msg = update.effective_message should_welc, cust_welcome, welc_type = sql.get_welc_pref(chat.id) welc_mutes = sql.welcome_mutes(chat.id) human_checks = sql.get_human_checks(user.id, chat.id) new_members = update.effective_message.new_chat_members for new_mem in new_members: welcome_log = None sent = None should_mute = True welcome_bool = True if should_welc: # Give the owner a special welcome if new_mem.id == OWNER_ID: update.effective_message.reply_text( "Woohoo, Has anyone Noticed Who came here??\nGuess......\n\nHe is My Master (The One Who Created Me) 🥰 \n\nWelcome Master❤️\nI am Feeling Proud that I was given a Chance to Welcome You.😜" ) welcome_log = (f"{html.escape(chat.title)}\n" f"#USER_JOINED\n" f"Bot Owner just joined the chat") # Welcome Devs elif new_mem.id in DEV_USERS: update.effective_message.reply_text( "Whoa! A member of the Heroes Association just joined!") # Welcome Sudos elif new_mem.id in SUDO_USERS: update.effective_message.reply_text( "Huh! A Dragon disaster just joined! Stay Alert!") # Welcome Support elif new_mem.id in SUPPORT_USERS: update.effective_message.reply_text( "Huh! Someone with a Demon disaster level just joined!") # Welcome Whitelisted elif new_mem.id in TIGER_USERS: update.effective_message.reply_text( "Oof! A Tiger disaster just joined!") # Welcome Tigers elif new_mem.id in WHITELIST_USERS: update.effective_message.reply_text( "Oof! A Wolf disaster just joined!") # Welcome yourself elif new_mem.id == bot.id: update.effective_message.reply_text( "Hello 😎 \n\n📌Important Instruction For @admin\n⚒ Please Make Me Admin with all the Permission, \nSo That, I Can Work Properly \n\nTo Know about Cmds Hit /help \n\nThank You" ) else: # If welcome message is media, send with appropriate function if welc_type not in (sql.Types.TEXT, sql.Types.BUTTON_TEXT): ENUM_FUNC_MAP[welc_type](chat.id, cust_welcome) continue # else, move on first_name = new_mem.first_name or "PersonWithNoName" # edge case of empty name - occurs for some bugs. if cust_welcome: if cust_welcome == sql.DEFAULT_WELCOME: cust_welcome = random.choice( sql.DEFAULT_WELCOME_MESSAGES).format( first=escape_markdown(first_name)) if new_mem.last_name: fullname = escape_markdown( f"{first_name} {new_mem.last_name}") else: fullname = escape_markdown(first_name) count = chat.get_members_count() mention = mention_markdown(new_mem.id, escape_markdown(first_name)) if new_mem.username: username = "******" + escape_markdown(new_mem.username) else: username = mention valid_format = escape_invalid_curly_brackets( cust_welcome, VALID_WELCOME_FORMATTERS) res = valid_format.format( first=escape_markdown(first_name), last=escape_markdown(new_mem.last_name or first_name), fullname=escape_markdown(fullname), username=username, mention=mention, count=count, chatname=escape_markdown(chat.title), id=new_mem.id) buttons = sql.get_welc_buttons(chat.id) keyb = build_keyboard(buttons) else: res = random.choice(sql.DEFAULT_WELCOME_MESSAGES).format( first=escape_markdown(first_name)) keyb = [] backup_message = random.choice( sql.DEFAULT_WELCOME_MESSAGES).format( first=escape_markdown(first_name)) keyboard = InlineKeyboardMarkup(keyb) else: welcome_bool = False res = None keyboard = None backup_message = None # User exceptions from welcomemutes if is_user_ban_protected(chat, new_mem.id, chat.get_member( new_mem.id)) or human_checks: should_mute = False # Join welcome: soft mute if new_mem.is_bot: should_mute = False if user.id == new_mem.id: if should_mute: if welc_mutes == "soft": bot.restrict_chat_member(chat.id, new_mem.id, can_send_messages=True, can_send_media_messages=False, can_send_other_messages=False, can_add_web_page_previews=False, until_date=(int(time.time() + 24 * 60 * 60))) if welc_mutes == "strong": welcome_bool = False VERIFIED_USER_WAITLIST.update({ new_mem.id: { "should_welc": should_welc, "status": False, "update": update, "res": res, "keyboard": keyboard, "backup_message": backup_message } }) new_join_mem = f"[{escape_markdown(new_mem.first_name)}](tg://user?id={user.id})" message = msg.reply_text( f"{new_join_mem}, click the button below to prove you're human.\nYou have 160 seconds.", reply_markup=InlineKeyboardMarkup([{ InlineKeyboardButton( text="Yes, I'm human.", callback_data=f"user_join_({new_mem.id})") }]), parse_mode=ParseMode.MARKDOWN) bot.restrict_chat_member(chat.id, new_mem.id, can_send_messages=False, can_send_media_messages=False, can_send_other_messages=False, can_add_web_page_previews=False) job_queue.run_once(partial(check_not_bot, new_mem, chat.id, message.message_id), 160, name="welcomemute") if welcome_bool: sent = send(update, res, keyboard, backup_message) prev_welc = sql.get_clean_pref(chat.id) if prev_welc: try: bot.delete_message(chat.id, prev_welc) except BadRequest: pass if sent: sql.set_clean_welcome(chat.id, sent.message_id) if welcome_log: return welcome_log return (f"{html.escape(chat.title)}\n" f"#USER_JOINED\n" f"<b>User</b>: {mention_html(user.id, user.first_name)}\n" f"<b>ID</b>: <code>{user.id}</code>") return ""
def setUp(self): self.jq = JobQueue(MockBot('jobqueue_test')) self.jq.start() self.result = 0 self.job_time = 0
def init(job_queue: JobQueue): job_queue.run_once(callback=startup, when=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) )
class JobQueueTest(BaseTest, unittest.TestCase): """ This object represents Tests for Updater, Dispatcher, WebhookServer and WebhookHandler """ def setUp(self): self.jq = JobQueue("Bot", tick_interval=0.005) self.result = 0 def tearDown(self): if self.jq is not None: self.jq.stop() def job1(self, bot): self.result += 1 def job2(self, bot): raise Exception("Test Error") def test_basic(self): self.jq.put(self.job1, 0.1) sleep(1.5) self.assertGreaterEqual(self.result, 10) def test_noRepeat(self): self.jq.put(self.job1, 0.1, repeat=False) sleep(0.5) self.assertEqual(1, self.result) def test_nextT(self): self.jq.put(self.job1, 0.1, next_t=0.5) sleep(0.45) self.assertEqual(0, self.result) sleep(0.1) self.assertEqual(1, self.result) def test_multiple(self): self.jq.put(self.job1, 0.1, repeat=False) self.jq.put(self.job1, 0.2, repeat=False) self.jq.put(self.job1, 0.4) sleep(1) self.assertEqual(4, self.result) def test_error(self): self.jq.put(self.job2, 0.1) self.jq.put(self.job1, 0.2) self.jq.start() sleep(0.4) self.assertEqual(1, self.result) def test_inUpdater(self): u = Updater(bot="MockBot", job_queue_tick_interval=0.005) u.job_queue.put(self.job1, 0.5) sleep(0.75) self.assertEqual(1, self.result) u.stop() sleep(2) self.assertEqual(1, self.result)
def create_jobs(job_classes: List[BaseJob], jq: JobQueue): for job in job_classes: jq.run_repeating(job.run, interval=job.get_interval(), first=job.get_delay())
class JobQueueTest(BaseTest, unittest.TestCase): """ This object represents Tests for Updater, Dispatcher, WebhookServer and WebhookHandler """ def setUp(self): self.jq = JobQueue(MockBot('jobqueue_test')) self.jq.start() self.result = 0 self.job_time = 0 def tearDown(self): if self.jq is not None: self.jq.stop() def getSeconds(self): return int(ceil(time.time())) def job1(self, bot, job): self.result += 1 def job2(self, bot, job): raise Exception("Test Error") def job3(self, bot, job): self.result += 1 job.schedule_removal() def job4(self, bot, job): self.result += job.context def job5(self, bot, job): self.job_time = self.getSeconds() def test_basic(self): self.jq.put(Job(self.job1, 0.1)) sleep(1.5) self.assertGreaterEqual(self.result, 10) def test_job_with_context(self): self.jq.put(Job(self.job4, 0.1, context=5)) sleep(1.5) self.assertGreaterEqual(self.result, 50) def test_noRepeat(self): self.jq.put(Job(self.job1, 0.1, repeat=False)) sleep(0.5) self.assertEqual(1, self.result) def test_nextT(self): self.jq.put(Job(self.job1, 0.1), next_t=0.5) sleep(0.45) self.assertEqual(0, self.result) sleep(0.1) self.assertEqual(1, self.result) def test_multiple(self): self.jq.put(Job(self.job1, 0.1, repeat=False)) self.jq.put(Job(self.job1, 0.2, repeat=False)) self.jq.put(Job(self.job1, 0.4)) sleep(1) self.assertEqual(4, self.result) def test_disabled(self): j0 = Job(self.job1, 0.1) j1 = Job(self.job1, 0.2) self.jq.put(j0) self.jq.put(Job(self.job1, 0.4)) self.jq.put(j1) j0.enabled = False j1.enabled = False sleep(1) self.assertEqual(2, self.result) def test_schedule_removal(self): j0 = Job(self.job1, 0.1) j1 = Job(self.job1, 0.2) self.jq.put(j0) self.jq.put(Job(self.job1, 0.4)) self.jq.put(j1) j0.schedule_removal() j1.schedule_removal() sleep(1) self.assertEqual(2, self.result) def test_schedule_removal_from_within(self): self.jq.put(Job(self.job1, 0.4)) self.jq.put(Job(self.job3, 0.2)) sleep(1) self.assertEqual(3, self.result) def test_longer_first(self): self.jq.put(Job(self.job1, 0.2, repeat=False)) self.jq.put(Job(self.job1, 0.1, repeat=False)) sleep(0.15) self.assertEqual(1, self.result) def test_error(self): self.jq.put(Job(self.job2, 0.1)) self.jq.put(Job(self.job1, 0.2)) sleep(0.5) self.assertEqual(2, self.result) def test_jobs_tuple(self): self.jq.stop() jobs = tuple(Job(self.job1, t) for t in range(5, 25)) for job in jobs: self.jq.put(job) self.assertTupleEqual(jobs, self.jq.jobs()) def test_inUpdater(self): u = Updater(bot="MockBot") u.job_queue.start() try: u.job_queue.put(Job(self.job1, 0.5)) sleep(0.75) self.assertEqual(1, self.result) u.stop() sleep(2) self.assertEqual(1, self.result) finally: u.stop() def test_time_unit_int(self): # Testing seconds in int seconds_interval = 5 expected_time = self.getSeconds() + seconds_interval self.jq.put(Job(self.job5, seconds_interval, repeat=False)) sleep(6) self.assertEqual(self.job_time, expected_time) def test_time_unit_dt_time(self): # Testing seconds, minutes and hours as datetime.timedelta object # This is sufficient to test that it actually works. interval = datetime.timedelta(seconds=5) expected_time = self.getSeconds() + interval.total_seconds() self.jq.put(Job(self.job5, interval, repeat=False)) sleep(6) self.assertEqual(self.job_time, expected_time)
def job_queue(bot): jq = JobQueue(bot) 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. 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`. 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. 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. """ _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: 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 = False, drop_pending_updates: bool = None, ip_address: str = None, ) -> 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. 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. 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): 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. """ 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, ) 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, force_event_loop=force_event_loop, ip_address=ip_address, ) 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 += 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, drop_pending_updates, webhook_url, allowed_updates, ready=None, force_event_loop=False, ip_address=None, ): 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. self._bootstrap( max_retries=bootstrap_retries, drop_pending_updates=drop_pending_updates, webhook_url=webhook_url, allowed_updates=allowed_updates, cert=open(cert, 'rb') if cert is not None else None, ip_address=ip_address, ) 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 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, ): 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, ) 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)
def setUp(self): self.jq = JobQueue(MockBot('jobqueue_test')) self.result = 0