diff --git a/bot.py b/bot.py index 94eda81..237e37f 100644 --- a/bot.py +++ b/bot.py @@ -1,3 +1,19 @@ # Work in Progress # Api-Key: 5228016873:AAGFrh0P6brag7oD3gxXjCh5gnLLE8JMvMs -# text bot at t.me/projektaktienbot \ No newline at end of file +# text bot at t.me/projektaktienbot +# API Documentation https://core.telegram.org/bots/api +# Code examples https://github.com/eternnoir/pyTelegramBotAPI#getting-started + +import telebot + +bot = telebot.TeleBot("5228016873:AAGFrh0P6brag7oD3gxXjCh5gnLLE8JMvMs") + +@bot.message_handler(commands=['start', 'help']) +def send_welcome(message): + bot.reply_to(message, "Thank you for using this bot") + +@bot.message_handler(func=lambda message: True) +def echo_all(message): + bot.reply_to(message, message.text) + +bot.infinity_polling() \ No newline at end of file diff --git a/telebot/__init__.py b/telebot/__init__.py new file mode 100644 index 0000000..9d4fde3 --- /dev/null +++ b/telebot/__init__.py @@ -0,0 +1,3887 @@ +# -*- coding: utf-8 -*- +from datetime import datetime + +import logging +import re +import sys +import threading +import time +import traceback +from typing import Any, Callable, List, Optional, Union + +# these imports are used to avoid circular import error +import telebot.util +import telebot.types + +# storage +from telebot.storage import StatePickleStorage, StateMemoryStorage + + + +logger = logging.getLogger('TeleBot') + +formatter = logging.Formatter( + '%(asctime)s (%(filename)s:%(lineno)d %(threadName)s) %(levelname)s - %(name)s: "%(message)s"' +) + +import inspect + +console_output_handler = logging.StreamHandler(sys.stderr) +console_output_handler.setFormatter(formatter) +logger.addHandler(console_output_handler) + +logger.setLevel(logging.ERROR) + +from telebot import apihelper, util, types +from telebot.handler_backends import MemoryHandlerBackend, FileHandlerBackend, BaseMiddleware, CancelUpdate, SkipHandler +from telebot.custom_filters import SimpleCustomFilter, AdvancedCustomFilter + + +REPLY_MARKUP_TYPES = Union[ + types.InlineKeyboardMarkup, types.ReplyKeyboardMarkup, + types.ReplyKeyboardRemove, types.ForceReply] + + +""" +Module : telebot +""" + + +class Handler: + """ + Class for (next step|reply) handlers + """ + + def __init__(self, callback, *args, **kwargs): + self.callback = callback + self.args = args + self.kwargs = kwargs + + def __getitem__(self, item): + return getattr(self, item) + + +class ExceptionHandler: + """ + Class for handling exceptions while Polling + """ + + # noinspection PyMethodMayBeStatic,PyUnusedLocal + def handle(self, exception): + return False + + +class TeleBot: + """ + This is the main synchronous class for Bot. + + It allows you to add handlers for different kind of updates. + + Usage: + + .. code-block:: python + + from telebot import TeleBot + bot = TeleBot('token') # get token from @BotFather + + See more examples in examples/ directory: + https://github.com/eternnoir/pyTelegramBotAPI/tree/master/examples + + """ + + def __init__( + self, token, parse_mode=None, threaded=True, skip_pending=False, num_threads=2, + next_step_backend=None, reply_backend=None, exception_handler=None, last_update_id=0, + suppress_middleware_excepions=False, state_storage=StateMemoryStorage(), use_class_middlewares=False + ): + """ + :param token: bot API token + :param parse_mode: default parse_mode + :return: Telebot object. + """ + self.token = token + self.parse_mode = parse_mode + self.update_listener = [] + self.skip_pending = skip_pending + self.suppress_middleware_excepions = suppress_middleware_excepions + + self.__stop_polling = threading.Event() + self.last_update_id = last_update_id + self.exc_info = None + + self.next_step_backend = next_step_backend + if not self.next_step_backend: + self.next_step_backend = MemoryHandlerBackend() + + self.reply_backend = reply_backend + if not self.reply_backend: + self.reply_backend = MemoryHandlerBackend() + + self.exception_handler = exception_handler + + self.message_handlers = [] + self.edited_message_handlers = [] + self.channel_post_handlers = [] + self.edited_channel_post_handlers = [] + self.inline_handlers = [] + self.chosen_inline_handlers = [] + self.callback_query_handlers = [] + self.shipping_query_handlers = [] + self.pre_checkout_query_handlers = [] + self.poll_handlers = [] + self.poll_answer_handlers = [] + self.my_chat_member_handlers = [] + self.chat_member_handlers = [] + self.chat_join_request_handlers = [] + self.custom_filters = {} + self.state_handlers = [] + + self.current_states = state_storage + + self.use_class_middlewares = use_class_middlewares + if apihelper.ENABLE_MIDDLEWARE and not use_class_middlewares: + self.typed_middleware_handlers = { + 'message': [], + 'edited_message': [], + 'channel_post': [], + 'edited_channel_post': [], + 'inline_query': [], + 'chosen_inline_result': [], + 'callback_query': [], + 'shipping_query': [], + 'pre_checkout_query': [], + 'poll': [], + 'poll_answer': [], + 'my_chat_member': [], + 'chat_member': [], + 'chat_join_request': [] + } + self.default_middleware_handlers = [] + if apihelper.ENABLE_MIDDLEWARE and use_class_middlewares: + logger.warning( + 'You are using class based middlewares, but you have ' + 'ENABLE_MIDDLEWARE set to True. This is not recommended.' + ) + self.middlewares = [] if use_class_middlewares else None + + + self.threaded = threaded + if self.threaded: + self.worker_pool = util.ThreadPool(self, num_threads=num_threads) + + @property + def user(self) -> types.User: + """ + The User object representing this bot. + Equivalent to bot.get_me() but the result is cached so only one API call is needed + """ + if not hasattr(self, "_user"): + self._user = self.get_me() + return self._user + + def enable_save_next_step_handlers(self, delay=120, filename="./.handler-saves/step.save"): + """ + Enable saving next step handlers (by default saving disabled) + + This function explicitly assigns FileHandlerBackend (instead of Saver) just to keep backward + compatibility whose purpose was to enable file saving capability for handlers. And the same + implementation is now available with FileHandlerBackend + + Most probably this function should be deprecated in future major releases + + :param delay: Delay between changes in handlers and saving + :param filename: Filename of save file + """ + self.next_step_backend = FileHandlerBackend(self.next_step_backend.handlers, filename, delay) + + def enable_saving_states(self, filename="./.state-save/states.pkl"): + """ + Enable saving states (by default saving disabled) + + :param filename: Filename of saving file + """ + self.current_states = StatePickleStorage(file_path=filename) + self.current_states.create_dir() + + def enable_save_reply_handlers(self, delay=120, filename="./.handler-saves/reply.save"): + """ + Enable saving reply handlers (by default saving disable) + + This function explicitly assigns FileHandlerBackend (instead of Saver) just to keep backward + compatibility whose purpose was to enable file saving capability for handlers. And the same + implementation is now available with FileHandlerBackend + + Most probably this function should be deprecated in future major releases + + :param delay: Delay between changes in handlers and saving + :param filename: Filename of save file + """ + self.reply_backend = FileHandlerBackend(self.reply_backend.handlers, filename, delay) + + def disable_save_next_step_handlers(self): + """ + Disable saving next step handlers (by default saving disable) + + This function is left to keep backward compatibility whose purpose was to disable file saving capability + for handlers. For the same purpose, MemoryHandlerBackend is reassigned as a new next_step_backend backend + instead of FileHandlerBackend. + + Most probably this function should be deprecated in future major releases + """ + self.next_step_backend = MemoryHandlerBackend(self.next_step_backend.handlers) + + def disable_save_reply_handlers(self): + """ + Disable saving next step handlers (by default saving disable) + + This function is left to keep backward compatibility whose purpose was to disable file saving capability + for handlers. For the same purpose, MemoryHandlerBackend is reassigned as a new reply_backend backend + instead of FileHandlerBackend. + + Most probably this function should be deprecated in future major releases + """ + self.reply_backend = MemoryHandlerBackend(self.reply_backend.handlers) + + def load_next_step_handlers(self, filename="./.handler-saves/step.save", del_file_after_loading=True): + """ + Load next step handlers from save file + + This function is left to keep backward compatibility whose purpose was to load handlers from file with the + help of FileHandlerBackend and is only recommended to use if next_step_backend was assigned as + FileHandlerBackend before entering this function + + Most probably this function should be deprecated in future major releases + + :param filename: Filename of the file where handlers was saved + :param del_file_after_loading: Is passed True, after loading save file will be deleted + """ + self.next_step_backend.load_handlers(filename, del_file_after_loading) + + def load_reply_handlers(self, filename="./.handler-saves/reply.save", del_file_after_loading=True): + """ + Load reply handlers from save file + + This function is left to keep backward compatibility whose purpose was to load handlers from file with the + help of FileHandlerBackend and is only recommended to use if reply_backend was assigned as + FileHandlerBackend before entering this function + + Most probably this function should be deprecated in future major releases + + :param filename: Filename of the file where handlers was saved + :param del_file_after_loading: Is passed True, after loading save file will be deleted + """ + self.reply_backend.load_handlers(filename, del_file_after_loading) + + def set_webhook(self, url=None, certificate=None, max_connections=None, allowed_updates=None, ip_address=None, + drop_pending_updates = None, timeout=None): + """ + Use this method to specify a url and receive incoming updates via an outgoing webhook. Whenever there is an + update for the bot, we will send an HTTPS POST request to the specified url, + containing a JSON-serialized Update. + In case of an unsuccessful request, we will give up after a reasonable amount of attempts. + Returns True on success. + + Telegram documentation: https://core.telegram.org/bots/api#setwebhook + + :param url: HTTPS url to send updates to. Use an empty string to remove webhook integration + :param certificate: Upload your public key certificate so that the root certificate in use can be checked. + See our self-signed guide for details. + :param max_connections: Maximum allowed number of simultaneous HTTPS connections to the webhook + for update delivery, 1-100. Defaults to 40. Use lower values to limit the load on your bot's server, + and higher values to increase your bot's throughput. + :param allowed_updates: A JSON-serialized list of the update types you want your bot to receive. + For example, specify [“message”, “edited_channel_post”, “callback_query”] to only receive updates + of these types. See Update for a complete list of available update types. + Specify an empty list to receive all updates regardless of type (default). + If not specified, the previous setting will be used. + :param ip_address: The fixed IP address which will be used to send webhook requests instead of the IP address + resolved through DNS + :param drop_pending_updates: Pass True to drop all pending updates + :param timeout: Integer. Request connection timeout + :return: API reply. + """ + return apihelper.set_webhook(self.token, url, certificate, max_connections, allowed_updates, ip_address, + drop_pending_updates, timeout) + + def delete_webhook(self, drop_pending_updates=None, timeout=None): + """ + Use this method to remove webhook integration if you decide to switch back to getUpdates. + + Telegram documentation: https://core.telegram.org/bots/api#deletewebhook + + :param drop_pending_updates: Pass True to drop all pending updates + :param timeout: Integer. Request connection timeout + :return: bool + """ + return apihelper.delete_webhook(self.token, drop_pending_updates, timeout) + + def get_webhook_info(self, timeout: Optional[int]=None): + """ + Use this method to get current webhook status. Requires no parameters. + If the bot is using getUpdates, will return an object with the url field empty. + + Telegram documentation: https://core.telegram.org/bots/api#getwebhookinfo + + :param timeout: Integer. Request connection timeout + :return: On success, returns a WebhookInfo object. + """ + result = apihelper.get_webhook_info(self.token, timeout) + return types.WebhookInfo.de_json(result) + + def remove_webhook(self): + return self.set_webhook() # No params resets webhook + + def get_updates(self, offset: Optional[int]=None, limit: Optional[int]=None, + timeout: Optional[int]=20, allowed_updates: Optional[List[str]]=None, + long_polling_timeout: int=20) -> List[types.Update]: + """ + Use this method to receive incoming updates using long polling (wiki). An Array of Update objects is returned. + + Telegram documentation: https://core.telegram.org/bots/api#getupdates + + :param allowed_updates: Array of string. List the types of updates you want your bot to receive. + :param offset: Integer. Identifier of the first update to be returned. + :param limit: Integer. Limits the number of updates to be retrieved. + :param timeout: Integer. Request connection timeout + :param long_polling_timeout: Timeout in seconds for long polling. + :return: array of Updates + """ + json_updates = apihelper.get_updates(self.token, offset, limit, timeout, allowed_updates, long_polling_timeout) + return [types.Update.de_json(ju) for ju in json_updates] + + def __skip_updates(self): + """ + Get and discard all pending updates before first poll of the bot + + :return: + """ + self.get_updates(offset=-1) + + def __retrieve_updates(self, timeout=20, long_polling_timeout=20, allowed_updates=None): + """ + Retrieves any updates from the Telegram API. + Registered listeners and applicable message handlers will be notified when a new message arrives. + + :raises ApiException when a call has failed. + """ + if self.skip_pending: + self.__skip_updates() + logger.debug('Skipped all pending messages') + self.skip_pending = False + updates = self.get_updates(offset=(self.last_update_id + 1), + allowed_updates=allowed_updates, + timeout=timeout, long_polling_timeout=long_polling_timeout) + self.process_new_updates(updates) + + def process_new_updates(self, updates): + """ + Processes new updates. Just pass list of subclasses of Update to this method. + + :param updates: List of Update objects + """ + upd_count = len(updates) + logger.debug('Received {0} new updates'.format(upd_count)) + if upd_count == 0: return + + new_messages = None + new_edited_messages = None + new_channel_posts = None + new_edited_channel_posts = None + new_inline_queries = None + new_chosen_inline_results = None + new_callback_queries = None + new_shipping_queries = None + new_pre_checkout_queries = None + new_polls = None + new_poll_answers = None + new_my_chat_members = None + new_chat_members = None + chat_join_request = None + + for update in updates: + if apihelper.ENABLE_MIDDLEWARE: + try: + self.process_middlewares(update) + except Exception as e: + logger.error(str(e)) + if not self.suppress_middleware_excepions: + raise + else: + if update.update_id > self.last_update_id: self.last_update_id = update.update_id + continue + + + if update.update_id > self.last_update_id: + self.last_update_id = update.update_id + if update.message: + if new_messages is None: new_messages = [] + new_messages.append(update.message) + if update.edited_message: + if new_edited_messages is None: new_edited_messages = [] + new_edited_messages.append(update.edited_message) + if update.channel_post: + if new_channel_posts is None: new_channel_posts = [] + new_channel_posts.append(update.channel_post) + if update.edited_channel_post: + if new_edited_channel_posts is None: new_edited_channel_posts = [] + new_edited_channel_posts.append(update.edited_channel_post) + if update.inline_query: + if new_inline_queries is None: new_inline_queries = [] + new_inline_queries.append(update.inline_query) + if update.chosen_inline_result: + if new_chosen_inline_results is None: new_chosen_inline_results = [] + new_chosen_inline_results.append(update.chosen_inline_result) + if update.callback_query: + if new_callback_queries is None: new_callback_queries = [] + new_callback_queries.append(update.callback_query) + if update.shipping_query: + if new_shipping_queries is None: new_shipping_queries = [] + new_shipping_queries.append(update.shipping_query) + if update.pre_checkout_query: + if new_pre_checkout_queries is None: new_pre_checkout_queries = [] + new_pre_checkout_queries.append(update.pre_checkout_query) + if update.poll: + if new_polls is None: new_polls = [] + new_polls.append(update.poll) + if update.poll_answer: + if new_poll_answers is None: new_poll_answers = [] + new_poll_answers.append(update.poll_answer) + if update.my_chat_member: + if new_my_chat_members is None: new_my_chat_members = [] + new_my_chat_members.append(update.my_chat_member) + if update.chat_member: + if new_chat_members is None: new_chat_members = [] + new_chat_members.append(update.chat_member) + if update.chat_join_request: + if chat_join_request is None: chat_join_request = [] + chat_join_request.append(update.chat_join_request) + + if new_messages: + self.process_new_messages(new_messages) + if new_edited_messages: + self.process_new_edited_messages(new_edited_messages) + if new_channel_posts: + self.process_new_channel_posts(new_channel_posts) + if new_edited_channel_posts: + self.process_new_edited_channel_posts(new_edited_channel_posts) + if new_inline_queries: + self.process_new_inline_query(new_inline_queries) + if new_chosen_inline_results: + self.process_new_chosen_inline_query(new_chosen_inline_results) + if new_callback_queries: + self.process_new_callback_query(new_callback_queries) + if new_shipping_queries: + self.process_new_shipping_query(new_shipping_queries) + if new_pre_checkout_queries: + self.process_new_pre_checkout_query(new_pre_checkout_queries) + if new_polls: + self.process_new_poll(new_polls) + if new_poll_answers: + self.process_new_poll_answer(new_poll_answers) + if new_my_chat_members: + self.process_new_my_chat_member(new_my_chat_members) + if new_chat_members: + self.process_new_chat_member(new_chat_members) + if chat_join_request: + self.process_new_chat_join_request(chat_join_request) + + def process_new_messages(self, new_messages): + self._notify_next_handlers(new_messages) + self._notify_reply_handlers(new_messages) + self.__notify_update(new_messages) + self._notify_command_handlers(self.message_handlers, new_messages, 'message') + + def process_new_edited_messages(self, edited_message): + self._notify_command_handlers(self.edited_message_handlers, edited_message, 'edited_message') + + def process_new_channel_posts(self, channel_post): + self._notify_command_handlers(self.channel_post_handlers, channel_post, 'channel_post') + + def process_new_edited_channel_posts(self, edited_channel_post): + self._notify_command_handlers(self.edited_channel_post_handlers, edited_channel_post, 'edited_channel_post') + + def process_new_inline_query(self, new_inline_querys): + self._notify_command_handlers(self.inline_handlers, new_inline_querys, 'inline_query') + + def process_new_chosen_inline_query(self, new_chosen_inline_querys): + self._notify_command_handlers(self.chosen_inline_handlers, new_chosen_inline_querys, 'chosen_inline_query') + + def process_new_callback_query(self, new_callback_querys): + self._notify_command_handlers(self.callback_query_handlers, new_callback_querys, 'callback_query') + + def process_new_shipping_query(self, new_shipping_querys): + self._notify_command_handlers(self.shipping_query_handlers, new_shipping_querys, 'shipping_query') + + def process_new_pre_checkout_query(self, pre_checkout_querys): + self._notify_command_handlers(self.pre_checkout_query_handlers, pre_checkout_querys, 'pre_checkout_query') + + def process_new_poll(self, polls): + self._notify_command_handlers(self.poll_handlers, polls, 'poll') + + def process_new_poll_answer(self, poll_answers): + self._notify_command_handlers(self.poll_answer_handlers, poll_answers, 'poll_answer') + + def process_new_my_chat_member(self, my_chat_members): + self._notify_command_handlers(self.my_chat_member_handlers, my_chat_members, 'my_chat_member') + + def process_new_chat_member(self, chat_members): + self._notify_command_handlers(self.chat_member_handlers, chat_members, 'chat_member') + + def process_new_chat_join_request(self, chat_join_request): + self._notify_command_handlers(self.chat_join_request_handlers, chat_join_request, 'chat_join_request') + + def process_middlewares(self, update): + for update_type, middlewares in self.typed_middleware_handlers.items(): + if getattr(update, update_type) is not None: + for typed_middleware_handler in middlewares: + try: + typed_middleware_handler(self, getattr(update, update_type)) + except Exception as e: + e.args = e.args + (f'Typed middleware handler "{typed_middleware_handler.__qualname__}"',) + raise + + if len(self.default_middleware_handlers) > 0: + for default_middleware_handler in self.default_middleware_handlers: + try: + default_middleware_handler(self, update) + except Exception as e: + e.args = e.args + (f'Default middleware handler "{default_middleware_handler.__qualname__}"',) + raise + + def __notify_update(self, new_messages): + if len(self.update_listener) == 0: + return + for listener in self.update_listener: + self._exec_task(listener, new_messages) + + def infinity_polling(self, timeout: int=20, skip_pending: bool=False, long_polling_timeout: int=20, logger_level=logging.ERROR, + allowed_updates: Optional[List[str]]=None, *args, **kwargs): + """ + Wrap polling with infinite loop and exception handling to avoid bot stops polling. + + :param timeout: Request connection timeout + :param long_polling_timeout: Timeout in seconds for long polling (see API docs) + :param skip_pending: skip old updates + :param logger_level: Custom logging level for infinity_polling logging. + Use logger levels from logging as a value. None/NOTSET = no error logging + :param allowed_updates: A list of the update types you want your bot to receive. + For example, specify [“message”, “edited_channel_post”, “callback_query”] to only receive updates of these types. + See util.update_types for a complete list of available update types. + Specify an empty list to receive all update types except chat_member (default). + If not specified, the previous setting will be used. + + Please note that this parameter doesn't affect updates created before the call to the get_updates, + so unwanted updates may be received for a short period of time. + """ + if skip_pending: + self.__skip_updates() + + while not self.__stop_polling.is_set(): + try: + self.polling(none_stop=True, timeout=timeout, long_polling_timeout=long_polling_timeout, + allowed_updates=allowed_updates, *args, **kwargs) + except Exception as e: + if logger_level and logger_level >= logging.ERROR: + logger.error("Infinity polling exception: %s", str(e)) + if logger_level and logger_level >= logging.DEBUG: + logger.error("Exception traceback:\n%s", traceback.format_exc()) + time.sleep(3) + continue + if logger_level and logger_level >= logging.INFO: + logger.error("Infinity polling: polling exited") + if logger_level and logger_level >= logging.INFO: + logger.error("Break infinity polling") + + def polling(self, non_stop: bool=False, skip_pending=False, interval: int=0, timeout: int=20, + long_polling_timeout: int=20, allowed_updates: Optional[List[str]]=None, + none_stop: Optional[bool]=None): + """ + This function creates a new Thread that calls an internal __retrieve_updates function. + This allows the bot to retrieve Updates automagically and notify listeners and message handlers accordingly. + + Warning: Do not call this function more than once! + + Always get updates. + + :param interval: Delay between two update retrivals + :param non_stop: Do not stop polling when an ApiException occurs. + :param timeout: Request connection timeout + :param skip_pending: skip old updates + :param long_polling_timeout: Timeout in seconds for long polling (see API docs) + :param allowed_updates: A list of the update types you want your bot to receive. + For example, specify [“message”, “edited_channel_post”, “callback_query”] to only receive updates of these types. + See util.update_types for a complete list of available update types. + Specify an empty list to receive all update types except chat_member (default). + If not specified, the previous setting will be used. + + Please note that this parameter doesn't affect updates created before the call to the get_updates, + so unwanted updates may be received for a short period of time. + :param none_stop: Deprecated, use non_stop. Old typo f***up compatibility + :return: + """ + if none_stop is not None: + non_stop = none_stop + + if skip_pending: + self.__skip_updates() + + if self.threaded: + self.__threaded_polling(non_stop, interval, timeout, long_polling_timeout, allowed_updates) + else: + self.__non_threaded_polling(non_stop, interval, timeout, long_polling_timeout, allowed_updates) + + def __threaded_polling(self, non_stop=False, interval=0, timeout = None, long_polling_timeout = None, allowed_updates=None): + logger.info('Started polling.') + self.__stop_polling.clear() + error_interval = 0.25 + + polling_thread = util.WorkerThread(name="PollingThread") + or_event = util.OrEvent( + polling_thread.done_event, + polling_thread.exception_event, + self.worker_pool.exception_event + ) + + while not self.__stop_polling.wait(interval): + or_event.clear() + try: + polling_thread.put(self.__retrieve_updates, timeout, long_polling_timeout, allowed_updates=allowed_updates) + or_event.wait() # wait for polling thread finish, polling thread error or thread pool error + polling_thread.raise_exceptions() + self.worker_pool.raise_exceptions() + error_interval = 0.25 + except apihelper.ApiException as e: + if self.exception_handler is not None: + handled = self.exception_handler.handle(e) + else: + handled = False + if not handled: + logger.error(e) + if not non_stop: + self.__stop_polling.set() + logger.info("Exception occurred. Stopping.") + else: + # polling_thread.clear_exceptions() + # self.worker_pool.clear_exceptions() + logger.info("Waiting for {0} seconds until retry".format(error_interval)) + time.sleep(error_interval) + if error_interval * 2 < 60: + error_interval *= 2 + else: + error_interval = 60 + else: + # polling_thread.clear_exceptions() + # self.worker_pool.clear_exceptions() + time.sleep(error_interval) + polling_thread.clear_exceptions() #* + self.worker_pool.clear_exceptions() #* + except KeyboardInterrupt: + logger.info("KeyboardInterrupt received.") + self.__stop_polling.set() + break + except Exception as e: + if self.exception_handler is not None: + handled = self.exception_handler.handle(e) + else: + handled = False + if not handled: + polling_thread.stop() + polling_thread.clear_exceptions() #* + self.worker_pool.clear_exceptions() #* + raise e + else: + polling_thread.clear_exceptions() + self.worker_pool.clear_exceptions() + time.sleep(error_interval) + + polling_thread.stop() + polling_thread.clear_exceptions() #* + self.worker_pool.clear_exceptions() #* + logger.info('Stopped polling.') + + def __non_threaded_polling(self, non_stop=False, interval=0, timeout=None, long_polling_timeout=None, allowed_updates=None): + logger.info('Started polling.') + self.__stop_polling.clear() + error_interval = 0.25 + + while not self.__stop_polling.wait(interval): + try: + self.__retrieve_updates(timeout, long_polling_timeout, allowed_updates=allowed_updates) + error_interval = 0.25 + except apihelper.ApiException as e: + if self.exception_handler is not None: + handled = self.exception_handler.handle(e) + else: + handled = False + + if not handled: + logger.error(e) + if not non_stop: + self.__stop_polling.set() + logger.info("Exception occurred. Stopping.") + else: + logger.info("Waiting for {0} seconds until retry".format(error_interval)) + time.sleep(error_interval) + error_interval *= 2 + else: + time.sleep(error_interval) + except KeyboardInterrupt: + logger.info("KeyboardInterrupt received.") + self.__stop_polling.set() + break + except Exception as e: + if self.exception_handler is not None: + handled = self.exception_handler.handle(e) + else: + handled = False + if not handled: + raise e + else: + time.sleep(error_interval) + + logger.info('Stopped polling.') + + def _exec_task(self, task, *args, **kwargs): + if kwargs and kwargs.get('task_type') == 'handler': + pass_bot = kwargs.get('pass_bot') + kwargs.pop('pass_bot') + kwargs.pop('task_type') + if pass_bot: + kwargs['bot'] = self + + if self.threaded: + self.worker_pool.put(task, *args, **kwargs) + else: + try: + task(*args, **kwargs) + except Exception as e: + if self.exception_handler is not None: + handled = self.exception_handler.handle(e) + else: + handled = False + if not handled: + raise e + + def stop_polling(self): + self.__stop_polling.set() + + def stop_bot(self): + self.stop_polling() + if self.threaded and self.worker_pool: + self.worker_pool.close() + + def set_update_listener(self, listener): + self.update_listener.append(listener) + + def get_me(self) -> types.User: + """ + Returns basic information about the bot in form of a User object. + + Telegram documentation: https://core.telegram.org/bots/api#getme + """ + result = apihelper.get_me(self.token) + return types.User.de_json(result) + + def get_file(self, file_id: str) -> types.File: + """ + Use this method to get basic info about a file and prepare it for downloading. + For the moment, bots can download files of up to 20MB in size. + On success, a File object is returned. + It is guaranteed that the link will be valid for at least 1 hour. + When the link expires, a new one can be requested by calling get_file again. + + Telegram documentation: https://core.telegram.org/bots/api#getfile + + :param file_id: File identifier + """ + return types.File.de_json(apihelper.get_file(self.token, file_id)) + + def get_file_url(self, file_id: str) -> str: + """ + Get a valid URL for downloading a file. + + :param file_id: File identifier to get download URL for. + """ + return apihelper.get_file_url(self.token, file_id) + + def download_file(self, file_path: str) -> bytes: + return apihelper.download_file(self.token, file_path) + + def log_out(self) -> bool: + """ + Use this method to log out from the cloud Bot API server before launching the bot locally. + You MUST log out the bot before running it locally, otherwise there is no guarantee + that the bot will receive updates. + After a successful call, you can immediately log in on a local server, + but will not be able to log in back to the cloud Bot API server for 10 minutes. + Returns True on success. + + Telegram documentation: https://core.telegram.org/bots/api#logout + """ + return apihelper.log_out(self.token) + + def close(self) -> bool: + """ + Use this method to close the bot instance before moving it from one local server to another. + You need to delete the webhook before calling this method to ensure that the bot isn't launched again + after server restart. + The method will return error 429 in the first 10 minutes after the bot is launched. + Returns True on success. + + Telegram documentation: https://core.telegram.org/bots/api#close + """ + return apihelper.close(self.token) + + def get_user_profile_photos(self, user_id: int, offset: Optional[int]=None, + limit: Optional[int]=None) -> types.UserProfilePhotos: + """ + Retrieves the user profile photos of the person with 'user_id' + + Telegram documentation: https://core.telegram.org/bots/api#getuserprofilephotos + + :param user_id: Integer - Unique identifier of the target user + :param offset: + :param limit: + :return: API reply. + """ + result = apihelper.get_user_profile_photos(self.token, user_id, offset, limit) + return types.UserProfilePhotos.de_json(result) + + def get_chat(self, chat_id: Union[int, str]) -> types.Chat: + """ + Use this method to get up to date information about the chat (current name of the user for one-on-one + conversations, current username of a user, group or channel, etc.). Returns a Chat object on success. + + Telegram documentation: https://core.telegram.org/bots/api#getchat + + :param chat_id: + :return: API reply. + """ + result = apihelper.get_chat(self.token, chat_id) + return types.Chat.de_json(result) + + def leave_chat(self, chat_id: Union[int, str]) -> bool: + """ + Use this method for your bot to leave a group, supergroup or channel. Returns True on success. + + Telegram documentation: https://core.telegram.org/bots/api#leavechat + + :param chat_id: + :return: API reply. + """ + result = apihelper.leave_chat(self.token, chat_id) + return result + + def get_chat_administrators(self, chat_id: Union[int, str]) -> List[types.ChatMember]: + """ + Use this method to get a list of administrators in a chat. + On success, returns an Array of ChatMember objects that contains + information about all chat administrators except other bots. + + Telegram documentation: https://core.telegram.org/bots/api#getchatadministrators + + :param chat_id: Unique identifier for the target chat or username + of the target supergroup or channel (in the format @channelusername) + :return: API reply. + """ + result = apihelper.get_chat_administrators(self.token, chat_id) + return [types.ChatMember.de_json(r) for r in result] + + def get_chat_members_count(self, chat_id: Union[int, str]) -> int: + """ + This function is deprecated. Use `get_chat_member_count` instead + """ + logger.info('get_chat_members_count is deprecated. Use get_chat_member_count instead.') + result = apihelper.get_chat_member_count(self.token, chat_id) + return result + + def get_chat_member_count(self, chat_id: Union[int, str]) -> int: + """ + Use this method to get the number of members in a chat. Returns Int on success. + + Telegram documentation: https://core.telegram.org/bots/api#getchatmembercount + + :param chat_id: + :return: API reply. + """ + result = apihelper.get_chat_member_count(self.token, chat_id) + return result + + def set_chat_sticker_set(self, chat_id: Union[int, str], sticker_set_name: str) -> types.StickerSet: + """ + Use this method to set a new group sticker set for a supergroup. The bot must be an administrator + in the chat for this to work and must have the appropriate admin rights. + Use the field can_set_sticker_set optionally returned in getChat requests to check + if the bot can use this method. Returns True on success. + + Telegram documentation: https://core.telegram.org/bots/api#setchatstickerset + + :param chat_id: Unique identifier for the target chat or username of the target supergroup (in the format @supergroupusername) + :param sticker_set_name: Name of the sticker set to be set as the group sticker set + :return: API reply. + """ + result = apihelper.set_chat_sticker_set(self.token, chat_id, sticker_set_name) + return result + + def delete_chat_sticker_set(self, chat_id: Union[int, str]) -> bool: + """ + Use this method to delete a group sticker set from a supergroup. The bot must be an administrator in the chat + for this to work and must have the appropriate admin rights. Use the field can_set_sticker_set + optionally returned in getChat requests to check if the bot can use this method. Returns True on success. + + Telegram documentation: https://core.telegram.org/bots/api#deletechatstickerset + + :param chat_id: Unique identifier for the target chat or username of the target supergroup (in the format @supergroupusername) + :return: API reply. + """ + result = apihelper.delete_chat_sticker_set(self.token, chat_id) + return result + + def get_chat_member(self, chat_id: Union[int, str], user_id: int) -> types.ChatMember: + """ + Use this method to get information about a member of a chat. Returns a ChatMember object on success. + + Telegram documentation: https://core.telegram.org/bots/api#getchatmember + + :param chat_id: + :param user_id: + :return: API reply. + """ + result = apihelper.get_chat_member(self.token, chat_id, user_id) + return types.ChatMember.de_json(result) + + def send_message( + self, chat_id: Union[int, str], text: str, + parse_mode: Optional[str]=None, + entities: Optional[List[types.MessageEntity]]=None, + disable_web_page_preview: Optional[bool]=None, + disable_notification: Optional[bool]=None, + protect_content: Optional[bool]=None, + reply_to_message_id: Optional[int]=None, + allow_sending_without_reply: Optional[bool]=None, + reply_markup: Optional[REPLY_MARKUP_TYPES]=None, + timeout: Optional[int]=None) -> types.Message: + """ + Use this method to send text messages. + + Warning: Do not send more than about 4000 characters each message, otherwise you'll risk an HTTP 414 error. + If you must send more than 4000 characters, + use the `split_string` or `smart_split` function in util.py. + + Telegram documentation: https://core.telegram.org/bots/api#sendmessage + + :param chat_id: Unique identifier for the target chat or username of the target channel (in the format @channelusername) + :param text: Text of the message to be sent + :param parse_mode: Send Markdown or HTML, if you want Telegram apps to show bold, italic, fixed-width text or inline URLs in your bot's message. + :param entities: List of special entities that appear in message text, which can be specified instead of parse_mode + :param disable_web_page_preview: Disables link previews for links in this message + :param disable_notification: Sends the message silently. Users will receive a notification with no sound. + :param protect_content: If True, the message content will be hidden for all users except for the target user + :param reply_to_message_id: If the message is a reply, ID of the original message + :param allow_sending_without_reply: Pass True, if the message should be sent even if the specified replied-to message is not found + :param reply_markup: Additional interface options. A JSON-serialized object for an inline keyboard, custom reply keyboard, instructions to remove reply keyboard or to force a reply from the user. + :param timeout: + :return: API reply. + """ + parse_mode = self.parse_mode if (parse_mode is None) else parse_mode + + return types.Message.de_json( + apihelper.send_message( + self.token, chat_id, text, disable_web_page_preview, reply_to_message_id, + reply_markup, parse_mode, disable_notification, timeout, + entities, allow_sending_without_reply, protect_content=protect_content)) + + def forward_message( + self, chat_id: Union[int, str], from_chat_id: Union[int, str], + message_id: int, disable_notification: Optional[bool]=None, + protect_content: Optional[bool]=None, + timeout: Optional[int]=None) -> types.Message: + """ + Use this method to forward messages of any kind. + + Telegram documentation: https://core.telegram.org/bots/api#forwardmessage + + :param disable_notification: + :param chat_id: which chat to forward + :param from_chat_id: which chat message from + :param message_id: message id + :param protect_content: Protects the contents of the forwarded message from forwarding and saving + :param timeout: + :return: API reply. + """ + return types.Message.de_json( + apihelper.forward_message(self.token, chat_id, from_chat_id, message_id, disable_notification, timeout, protect_content)) + + def copy_message( + self, chat_id: Union[int, str], + from_chat_id: Union[int, str], + message_id: int, + caption: Optional[str]=None, + parse_mode: Optional[str]=None, + caption_entities: Optional[List[types.MessageEntity]]=None, + disable_notification: Optional[bool]=None, + protect_content: Optional[bool]=None, + reply_to_message_id: Optional[int]=None, + allow_sending_without_reply: Optional[bool]=None, + reply_markup: Optional[REPLY_MARKUP_TYPES]=None, + timeout: Optional[int]=None) -> int: + """ + Use this method to copy messages of any kind. + + Telegram documentation: https://core.telegram.org/bots/api#copymessage + + :param chat_id: which chat to forward + :param from_chat_id: which chat message from + :param message_id: message id + :param caption: + :param parse_mode: + :param caption_entities: + :param disable_notification: + :param protect_content: + :param reply_to_message_id: + :param allow_sending_without_reply: + :param reply_markup: + :param timeout: + :return: API reply. + """ + return types.MessageID.de_json( + apihelper.copy_message(self.token, chat_id, from_chat_id, message_id, caption, parse_mode, caption_entities, + disable_notification, reply_to_message_id, allow_sending_without_reply, reply_markup, + timeout, protect_content)) + + def delete_message(self, chat_id: Union[int, str], message_id: int, + timeout: Optional[int]=None) -> bool: + """ + Use this method to delete message. Returns True on success. + + Telegram documentation: https://core.telegram.org/bots/api#deletemessage + + :param chat_id: in which chat to delete + :param message_id: which message to delete + :param timeout: + :return: API reply. + """ + return apihelper.delete_message(self.token, chat_id, message_id, timeout) + + def send_dice( + self, chat_id: Union[int, str], + emoji: Optional[str]=None, disable_notification: Optional[bool]=None, + reply_to_message_id: Optional[int]=None, + reply_markup: Optional[REPLY_MARKUP_TYPES]=None, + timeout: Optional[int]=None, + allow_sending_without_reply: Optional[bool]=None, + protect_content: Optional[bool]=None) -> types.Message: + """ + Use this method to send dices. + + Telegram documentation: https://core.telegram.org/bots/api#senddice + + :param chat_id: + :param emoji: + :param disable_notification: + :param reply_to_message_id: + :param reply_markup: + :param timeout: + :param allow_sending_without_reply: + :param protect_content: + :return: Message + """ + return types.Message.de_json( + apihelper.send_dice( + self.token, chat_id, emoji, disable_notification, reply_to_message_id, + reply_markup, timeout, allow_sending_without_reply, protect_content) + ) + + def send_photo( + self, chat_id: Union[int, str], photo: Union[Any, str], + caption: Optional[str]=None, parse_mode: Optional[str]=None, + caption_entities: Optional[List[types.MessageEntity]]=None, + disable_notification: Optional[bool]=None, + protect_content: Optional[bool]=None, + reply_to_message_id: Optional[int]=None, + allow_sending_without_reply: Optional[bool]=None, + reply_markup: Optional[REPLY_MARKUP_TYPES]=None, + timeout: Optional[int]=None,) -> types.Message: + """ + Use this method to send photos. On success, the sent Message is returned. + + Telegram documentation: https://core.telegram.org/bots/api#sendphoto + + :param chat_id: + :param photo: + :param caption: + :param parse_mode: + :param caption_entities: + :param disable_notification: + :param protect_content: + :param reply_to_message_id: + :param allow_sending_without_reply: + :param reply_markup: + :param timeout: + :return: Message + """ + parse_mode = self.parse_mode if (parse_mode is None) else parse_mode + + return types.Message.de_json( + apihelper.send_photo( + self.token, chat_id, photo, caption, reply_to_message_id, reply_markup, + parse_mode, disable_notification, timeout, caption_entities, + allow_sending_without_reply, protect_content)) + + # TODO: Rewrite this method like in API. + def send_audio( + self, chat_id: Union[int, str], audio: Union[Any, str], + caption: Optional[str]=None, duration: Optional[int]=None, + performer: Optional[str]=None, title: Optional[str]=None, + reply_to_message_id: Optional[int]=None, + reply_markup: Optional[REPLY_MARKUP_TYPES]=None, + parse_mode: Optional[str]=None, + disable_notification: Optional[bool]=None, + timeout: Optional[int]=None, + thumb: Optional[Union[Any, str]]=None, + caption_entities: Optional[List[types.MessageEntity]]=None, + allow_sending_without_reply: Optional[bool]=None, + protect_content: Optional[bool]=None) -> types.Message: + """ + Use this method to send audio files, if you want Telegram clients to display them in the music player. + Your audio must be in the .mp3 format. + + Telegram documentation: https://core.telegram.org/bots/api#sendaudio + + :param chat_id:Unique identifier for the message recipient + :param audio:Audio file to send. + :param caption: + :param duration:Duration of the audio in seconds + :param performer:Performer + :param title:Track name + :param reply_to_message_id:If the message is a reply, ID of the original message + :param reply_markup: + :param parse_mode + :param disable_notification: + :param timeout: + :param thumb: + :param caption_entities: + :param allow_sending_without_reply: + :param protect_content: + :return: Message + """ + parse_mode = self.parse_mode if (parse_mode is None) else parse_mode + + return types.Message.de_json( + apihelper.send_audio( + self.token, chat_id, audio, caption, duration, performer, title, reply_to_message_id, + reply_markup, parse_mode, disable_notification, timeout, thumb, + caption_entities, allow_sending_without_reply, protect_content)) + + # TODO: Rewrite this method like in API. + def send_voice( + self, chat_id: Union[int, str], voice: Union[Any, str], + caption: Optional[str]=None, duration: Optional[int]=None, + reply_to_message_id: Optional[int]=None, + reply_markup: Optional[REPLY_MARKUP_TYPES]=None, + parse_mode: Optional[str]=None, + disable_notification: Optional[bool]=None, + timeout: Optional[int]=None, + caption_entities: Optional[List[types.MessageEntity]]=None, + allow_sending_without_reply: Optional[bool]=None, + protect_content: Optional[bool]=None) -> types.Message: + """ + Use this method to send audio files, if you want Telegram clients to display the file + as a playable voice message. + + Telegram documentation: https://core.telegram.org/bots/api#sendvoice + + :param chat_id: Unique identifier for the message recipient. + :param voice: + :param caption: + :param duration: Duration of sent audio in seconds + :param reply_to_message_id: + :param reply_markup: + :param parse_mode: + :param disable_notification: + :param timeout: + :param caption_entities: + :param allow_sending_without_reply: + :param protect_content: + :return: Message + """ + parse_mode = self.parse_mode if (parse_mode is None) else parse_mode + + return types.Message.de_json( + apihelper.send_voice( + self.token, chat_id, voice, caption, duration, reply_to_message_id, reply_markup, + parse_mode, disable_notification, timeout, caption_entities, + allow_sending_without_reply, protect_content)) + + # TODO: Rewrite this method like in API. + def send_document( + self, chat_id: Union[int, str], document: Union[Any, str], + reply_to_message_id: Optional[int]=None, + caption: Optional[str]=None, + reply_markup: Optional[REPLY_MARKUP_TYPES]=None, + parse_mode: Optional[str]=None, + disable_notification: Optional[bool]=None, + timeout: Optional[int]=None, + thumb: Optional[Union[Any, str]]=None, + caption_entities: Optional[List[types.MessageEntity]]=None, + allow_sending_without_reply: Optional[bool]=None, + visible_file_name: Optional[str]=None, + disable_content_type_detection: Optional[bool]=None, + data: Optional[Union[Any, str]]=None, + protect_content: Optional[bool]=None) -> types.Message: + """ + Use this method to send general files. + + Telegram documentation: https://core.telegram.org/bots/api#senddocument + + :param chat_id: Unique identifier for the target chat or username of the target channel (in the format @channelusername) + :param document: (document) File to send. Pass a file_id as String to send a file that exists on the Telegram servers (recommended), pass an HTTP URL as a String for Telegram to get a file from the Internet, or upload a new one using multipart/form-data + :param reply_to_message_id: If the message is a reply, ID of the original message + :param caption: Document caption (may also be used when resending documents by file_id), 0-1024 characters after entities parsing + :param reply_markup: + :param parse_mode: Mode for parsing entities in the document caption + :param disable_notification: Sends the message silently. Users will receive a notification with no sound. + :param timeout: + :param thumb: InputFile or String : Thumbnail of the file sent; can be ignored if thumbnail generation for the file is supported server-side. The thumbnail should be in JPEG format and less than 200 kB in size. A thumbnail's width and height should not exceed 320. Ignored if the file is not uploaded using multipart/form-data. Thumbnails can't be reused and can be only uploaded as a new file, so you can pass “attach://” if the thumbnail was uploaded using multipart/form-data under + :param caption_entities: + :param allow_sending_without_reply: + :param visible_file_name: allows to define file name that will be visible in the Telegram instead of original file name + :param disable_content_type_detection: Disables automatic server-side content type detection for files uploaded using multipart/form-data + :param data: function typo miss compatibility: do not use it + :param protect_content: + :return: API reply. + """ + parse_mode = self.parse_mode if (parse_mode is None) else parse_mode + if data and not(document): + # function typo miss compatibility + document = data + + return types.Message.de_json( + apihelper.send_data( + self.token, chat_id, document, 'document', + reply_to_message_id = reply_to_message_id, reply_markup = reply_markup, parse_mode = parse_mode, + disable_notification = disable_notification, timeout = timeout, caption = caption, thumb = thumb, + caption_entities = caption_entities, allow_sending_without_reply = allow_sending_without_reply, + disable_content_type_detection = disable_content_type_detection, visible_file_name = visible_file_name, + protect_content = protect_content)) + + # TODO: Rewrite this method like in API. + def send_sticker( + self, chat_id: Union[int, str], + sticker: Union[Any, str], + reply_to_message_id: Optional[int]=None, + reply_markup: Optional[REPLY_MARKUP_TYPES]=None, + disable_notification: Optional[bool]=None, + timeout: Optional[int]=None, + allow_sending_without_reply: Optional[bool]=None, + protect_content:Optional[bool]=None, + data: Union[Any, str]=None) -> types.Message: + """ + Use this method to send .webp stickers. + + Telegram documentation: https://core.telegram.org/bots/api#sendsticker + + :param chat_id: + :param sticker: + :param data: + :param reply_to_message_id: + :param reply_markup: + :param disable_notification: to disable the notification + :param timeout: timeout + :param allow_sending_without_reply: + :param protect_content: + :param data: function typo miss compatibility: do not use it + :return: API reply. + """ + if data and not(sticker): + # function typo miss compatibility + sticker = data + return types.Message.de_json( + apihelper.send_data( + self.token, chat_id, sticker, 'sticker', + reply_to_message_id=reply_to_message_id, reply_markup=reply_markup, + disable_notification=disable_notification, timeout=timeout, + allow_sending_without_reply=allow_sending_without_reply, + protect_content=protect_content)) + + def send_video( + self, chat_id: Union[int, str], video: Union[Any, str], + duration: Optional[int]=None, + width: Optional[int]=None, + height: Optional[int]=None, + thumb: Optional[Union[Any, str]]=None, + caption: Optional[str]=None, + parse_mode: Optional[str]=None, + caption_entities: Optional[List[types.MessageEntity]]=None, + supports_streaming: Optional[bool]=None, + disable_notification: Optional[bool]=None, + protect_content: Optional[bool]=None, + reply_to_message_id: Optional[int]=None, + allow_sending_without_reply: Optional[bool]=None, + reply_markup: Optional[REPLY_MARKUP_TYPES]=None, + timeout: Optional[int]=None, + data: Optional[Union[Any, str]]=None) -> types.Message: + """ + Use this method to send video files, Telegram clients support mp4 videos (other formats may be sent as Document). + + Telegram documentation: https://core.telegram.org/bots/api#sendvideo + + :param chat_id: Unique identifier for the target chat or username of the target channel (in the format @channelusername) + :param video: Video to send. You can either pass a file_id as String to resend a video that is already on the Telegram servers, or upload a new video file using multipart/form-data. + :param duration: Duration of sent video in seconds + :param width: Video width + :param height: Video height + :param thumb: Thumbnail of the file sent; can be ignored if thumbnail generation for the file is supported server-side. The thumbnail should be in JPEG format and less than 200 kB in size. A thumbnail's width and height should not exceed 320. Ignored if the file is not uploaded using multipart/form-data. Thumbnails can't be reused and can be only uploaded as a new file, so you can pass “attach://” if the thumbnail was uploaded using multipart/form-data under . + :param caption: Video caption (may also be used when resending videos by file_id), 0-1024 characters after entities parsing + :param parse_mode: Mode for parsing entities in the video caption + :param caption_entities: + :param supports_streaming: Pass True, if the uploaded video is suitable for streaming + :param disable_notification: Sends the message silently. Users will receive a notification with no sound. + :param protect_content: + :param reply_to_message_id: If the message is a reply, ID of the original message + :param allow_sending_without_reply: + :param reply_markup: + :param timeout: + :param data: function typo miss compatibility: do not use it + """ + parse_mode = self.parse_mode if (parse_mode is None) else parse_mode + if data and not(video): + # function typo miss compatibility + video = data + + return types.Message.de_json( + apihelper.send_video( + self.token, chat_id, video, duration, caption, reply_to_message_id, reply_markup, + parse_mode, supports_streaming, disable_notification, timeout, thumb, width, height, + caption_entities, allow_sending_without_reply, protect_content)) + + def send_animation( + self, chat_id: Union[int, str], animation: Union[Any, str], + duration: Optional[int]=None, + width: Optional[int]=None, + height: Optional[int]=None, + thumb: Optional[Union[Any, str]]=None, + caption: Optional[str]=None, + parse_mode: Optional[str]=None, + caption_entities: Optional[List[types.MessageEntity]]=None, + disable_notification: Optional[bool]=None, + protect_content: Optional[bool]=None, + reply_to_message_id: Optional[int]=None, + allow_sending_without_reply: Optional[bool]=None, + reply_markup: Optional[REPLY_MARKUP_TYPES]=None, + timeout: Optional[int]=None, ) -> types.Message: + """ + Use this method to send animation files (GIF or H.264/MPEG-4 AVC video without sound). + + Telegram documentation: https://core.telegram.org/bots/api#sendanimation + + :param chat_id: Integer : Unique identifier for the message recipient — User or GroupChat id + :param animation: InputFile or String : Animation to send. You can either pass a file_id as String to resend an + animation that is already on the Telegram server + :param duration: Integer : Duration of sent video in seconds + :param width: Integer : Video width + :param height: Integer : Video height + :param thumb: InputFile or String : Thumbnail of the file sent + :param caption: String : Animation caption (may also be used when resending animation by file_id). + :param parse_mode: + :param protect_content: + :param reply_to_message_id: + :param reply_markup: + :param disable_notification: + :param timeout: + :param caption_entities: + :param allow_sending_without_reply: + :return: + """ + parse_mode = self.parse_mode if (parse_mode is None) else parse_mode + + return types.Message.de_json( + apihelper.send_animation( + self.token, chat_id, animation, duration, caption, reply_to_message_id, + reply_markup, parse_mode, disable_notification, timeout, thumb, + caption_entities, allow_sending_without_reply, protect_content, width, height)) + + # TODO: Rewrite this method like in API. + def send_video_note( + self, chat_id: Union[int, str], data: Union[Any, str], + duration: Optional[int]=None, + length: Optional[int]=None, + reply_to_message_id: Optional[int]=None, + reply_markup: Optional[REPLY_MARKUP_TYPES]=None, + disable_notification: Optional[bool]=None, + timeout: Optional[int]=None, + thumb: Optional[Union[Any, str]]=None, + allow_sending_without_reply: Optional[bool]=None, + protect_content: Optional[bool]=None) -> types.Message: + """ + As of v.4.0, Telegram clients support rounded square mp4 videos of up to 1 minute long. Use this method to send + video messages. + + Telegram documentation: https://core.telegram.org/bots/api#sendvideonote + + :param chat_id: Integer : Unique identifier for the message recipient — User or GroupChat id + :param data: InputFile or String : Video note to send. You can either pass a file_id as String to resend + a video that is already on the Telegram server + :param duration: Integer : Duration of sent video in seconds + :param length: Integer : Video width and height, Can't be None and should be in range of (0, 640) + :param reply_to_message_id: + :param reply_markup: + :param disable_notification: + :param timeout: + :param thumb: InputFile or String : Thumbnail of the file sent + :param allow_sending_without_reply: + :param protect_content: + :return: + """ + return types.Message.de_json( + apihelper.send_video_note( + self.token, chat_id, data, duration, length, reply_to_message_id, reply_markup, + disable_notification, timeout, thumb, allow_sending_without_reply, protect_content)) + + def send_media_group( + self, chat_id: Union[int, str], + media: List[Union[ + types.InputMediaAudio, types.InputMediaDocument, + types.InputMediaPhoto, types.InputMediaVideo]], + disable_notification: Optional[bool]=None, + protect_content: Optional[bool]=None, + reply_to_message_id: Optional[int]=None, + timeout: Optional[int]=None, + allow_sending_without_reply: Optional[bool]=None) -> List[types.Message]: + """ + Send a group of photos or videos as an album. On success, an array of the sent Messages is returned. + + Telegram documentation: https://core.telegram.org/bots/api#sendmediagroup + + :param chat_id: + :param media: + :param disable_notification: + :param protect_content: + :param reply_to_message_id: + :param timeout: + :param allow_sending_without_reply: + :return: + """ + result = apihelper.send_media_group( + self.token, chat_id, media, disable_notification, reply_to_message_id, timeout, + allow_sending_without_reply, protect_content) + return [types.Message.de_json(msg) for msg in result] + + # TODO: Rewrite this method like in API. + def send_location( + self, chat_id: Union[int, str], + latitude: float, longitude: float, + live_period: Optional[int]=None, + reply_to_message_id: Optional[int]=None, + reply_markup: Optional[REPLY_MARKUP_TYPES]=None, + disable_notification: Optional[bool]=None, + timeout: Optional[int]=None, + horizontal_accuracy: Optional[float]=None, + heading: Optional[int]=None, + proximity_alert_radius: Optional[int]=None, + allow_sending_without_reply: Optional[bool]=None, + protect_content: Optional[bool]=None) -> types.Message: + """ + Use this method to send point on the map. + + Telegram documentation: https://core.telegram.org/bots/api#sendlocation + + :param chat_id: + :param latitude: + :param longitude: + :param live_period: + :param reply_to_message_id: + :param reply_markup: + :param disable_notification: + :param timeout: + :param horizontal_accuracy: + :param heading: + :param proximity_alert_radius: + :param allow_sending_without_reply: + :param protect_content: + :return: API reply. + """ + return types.Message.de_json( + apihelper.send_location( + self.token, chat_id, latitude, longitude, live_period, + reply_to_message_id, reply_markup, disable_notification, timeout, + horizontal_accuracy, heading, proximity_alert_radius, + allow_sending_without_reply, protect_content)) + + def edit_message_live_location( + self, latitude: float, longitude: float, + chat_id: Optional[Union[int, str]]=None, + message_id: Optional[int]=None, + inline_message_id: Optional[str]=None, + reply_markup: Optional[REPLY_MARKUP_TYPES]=None, + timeout: Optional[int]=None, + horizontal_accuracy: Optional[float]=None, + heading: Optional[int]=None, + proximity_alert_radius: Optional[int]=None) -> types.Message: + """ + Use this method to edit live location. + + Telegram documentation: https://core.telegram.org/bots/api#editmessagelivelocation + + :param latitude: + :param longitude: + :param chat_id: + :param message_id: + :param reply_markup: + :param timeout: + :param inline_message_id: + :param horizontal_accuracy: + :param heading: + :param proximity_alert_radius: + :return: + """ + return types.Message.de_json( + apihelper.edit_message_live_location( + self.token, latitude, longitude, chat_id, message_id, + inline_message_id, reply_markup, timeout, + horizontal_accuracy, heading, proximity_alert_radius)) + + def stop_message_live_location( + self, chat_id: Optional[Union[int, str]]=None, + message_id: Optional[int]=None, + inline_message_id: Optional[str]=None, + reply_markup: Optional[REPLY_MARKUP_TYPES]=None, + timeout: Optional[int]=None) -> types.Message: + """ + Use this method to stop updating a live location message sent by the bot + or via the bot (for inline bots) before live_period expires + + Telegram documentation: https://core.telegram.org/bots/api#stopmessagelivelocation + + :param chat_id: + :param message_id: + :param inline_message_id: + :param reply_markup: + :param timeout: + :return: + """ + return types.Message.de_json( + apihelper.stop_message_live_location( + self.token, chat_id, message_id, inline_message_id, reply_markup, timeout)) + + # TODO: Rewrite this method like in API. + def send_venue( + self, chat_id: Union[int, str], + latitude: float, longitude: float, + title: str, address: str, + foursquare_id: Optional[str]=None, + foursquare_type: Optional[str]=None, + disable_notification: Optional[bool]=None, + reply_to_message_id: Optional[int]=None, + reply_markup: Optional[REPLY_MARKUP_TYPES]=None, + timeout: Optional[int]=None, + allow_sending_without_reply: Optional[bool]=None, + google_place_id: Optional[str]=None, + google_place_type: Optional[str]=None, + protect_content: Optional[bool]=None) -> types.Message: + """ + Use this method to send information about a venue. + + Telegram documentation: https://core.telegram.org/bots/api#sendvenue + + :param chat_id: Integer or String : Unique identifier for the target chat or username of the target channel + :param latitude: Float : Latitude of the venue + :param longitude: Float : Longitude of the venue + :param title: String : Name of the venue + :param address: String : Address of the venue + :param foursquare_id: String : Foursquare identifier of the venue + :param foursquare_type: Foursquare type of the venue, if known. (For example, “arts_entertainment/default”, + “arts_entertainment/aquarium” or “food/icecream”.) + :param disable_notification: + :param reply_to_message_id: + :param reply_markup: + :param timeout: + :param allow_sending_without_reply: + :param google_place_id: + :param google_place_type: + :param protect_content: + :return: + """ + return types.Message.de_json( + apihelper.send_venue( + self.token, chat_id, latitude, longitude, title, address, foursquare_id, foursquare_type, + disable_notification, reply_to_message_id, reply_markup, timeout, + allow_sending_without_reply, google_place_id, google_place_type, protect_content)) + + # TODO: Rewrite this method like in API. + def send_contact( + self, chat_id: Union[int, str], phone_number: str, + first_name: str, last_name: Optional[str]=None, + vcard: Optional[str]=None, + disable_notification: Optional[bool]=None, + reply_to_message_id: Optional[int]=None, + reply_markup: Optional[REPLY_MARKUP_TYPES]=None, + timeout: Optional[int]=None, + allow_sending_without_reply: Optional[bool]=None, + protect_content: Optional[bool]=None) -> types.Message: + """ + Use this method to send phone contacts. + + Telegram documentation: https://core.telegram.org/bots/api#sendcontact + + :param chat_id: Integer or String : Unique identifier for the target chat or username of the target channel + :param phone_number: String : Contact's phone number + :param first_name: String : Contact's first name + :param last_name: String : Contact's last name + :param vcard: String : Additional data about the contact in the form of a vCard, 0-2048 bytes + :param disable_notification: + :param reply_to_message_id: + :param reply_markup: + :param timeout: + :param allow_sending_without_reply: + :param protect_content: + :return: + """ + + return types.Message.de_json( + apihelper.send_contact( + self.token, chat_id, phone_number, first_name, last_name, vcard, + disable_notification, reply_to_message_id, reply_markup, timeout, + allow_sending_without_reply, protect_content)) + + def send_chat_action( + self, chat_id: Union[int, str], action: str, timeout: Optional[int]=None) -> bool: + """ + Use this method when you need to tell the user that something is happening on the bot's side. + The status is set for 5 seconds or less (when a message arrives from your bot, Telegram clients clear + its typing status). + + Telegram documentation: https://core.telegram.org/bots/api#sendchataction + + :param chat_id: + :param action: One of the following strings: 'typing', 'upload_photo', 'record_video', 'upload_video', + 'record_audio', 'upload_audio', 'upload_document', 'find_location', 'record_video_note', + 'upload_video_note'. + :param timeout: + :return: API reply. :type: boolean + """ + return apihelper.send_chat_action(self.token, chat_id, action, timeout) + + def kick_chat_member( + self, chat_id: Union[int, str], user_id: int, + until_date:Optional[Union[int, datetime]]=None, + revoke_messages: Optional[bool]=None) -> bool: + """ + This function is deprecated. Use `ban_chat_member` instead + """ + logger.info('kick_chat_member is deprecated. Use ban_chat_member instead.') + return apihelper.ban_chat_member(self.token, chat_id, user_id, until_date, revoke_messages) + + def ban_chat_member( + self, chat_id: Union[int, str], user_id: int, + until_date:Optional[Union[int, datetime]]=None, + revoke_messages: Optional[bool]=None) -> bool: + """ + Use this method to ban a user in a group, a supergroup or a channel. + In the case of supergroups and channels, the user will not be able to return to the chat on their + own using invite links, etc., unless unbanned first. + Returns True on success. + + Telegram documentation: https://core.telegram.org/bots/api#banchatmember + + :param chat_id: Int or string : Unique identifier for the target group or username of the target supergroup + :param user_id: Int : Unique identifier of the target user + :param until_date: Date when the user will be unbanned, unix time. If user is banned for more than 366 days or + less than 30 seconds from the current time they are considered to be banned forever + :param revoke_messages: Bool: Pass True to delete all messages from the chat for the user that is being removed. + If False, the user will be able to see messages in the group that were sent before the user was removed. + Always True for supergroups and channels. + :return: boolean + """ + return apihelper.ban_chat_member(self.token, chat_id, user_id, until_date, revoke_messages) + + def unban_chat_member( + self, chat_id: Union[int, str], user_id: int, + only_if_banned: Optional[bool]=False) -> bool: + """ + Use this method to unban a previously kicked user in a supergroup or channel. + The user will not return to the group or channel automatically, but will be able to join via link, etc. + The bot must be an administrator for this to work. By default, this method guarantees that after the call + the user is not a member of the chat, but will be able to join it. So if the user is a member of the chat + they will also be removed from the chat. If you don't want this, use the parameter only_if_banned. + + Telegram documentation: https://core.telegram.org/bots/api#unbanchatmember + + :param chat_id: Unique identifier for the target group or username of the target supergroup or channel + (in the format @username) + :param user_id: Unique identifier of the target user + :param only_if_banned: Do nothing if the user is not banned + :return: True on success + """ + return apihelper.unban_chat_member(self.token, chat_id, user_id, only_if_banned) + + def restrict_chat_member( + self, chat_id: Union[int, str], user_id: int, + until_date: Optional[Union[int, datetime]]=None, + can_send_messages: Optional[bool]=None, + can_send_media_messages: Optional[bool]=None, + can_send_polls: Optional[bool]=None, + can_send_other_messages: Optional[bool]=None, + can_add_web_page_previews: Optional[bool]=None, + can_change_info: Optional[bool]=None, + can_invite_users: Optional[bool]=None, + can_pin_messages: Optional[bool]=None) -> bool: + """ + Use this method to restrict a user in a supergroup. + The bot must be an administrator in the supergroup for this to work and must have + the appropriate admin rights. Pass True for all boolean parameters to lift restrictions from a user. + + Telegram documentation: https://core.telegram.org/bots/api#restrictchatmember + + :param chat_id: Int or String : Unique identifier for the target group or username of the target supergroup + or channel (in the format @channelusername) + :param user_id: Int : Unique identifier of the target user + :param until_date: Date when restrictions will be lifted for the user, unix time. + If user is restricted for more than 366 days or less than 30 seconds from the current time, + they are considered to be restricted forever + :param can_send_messages: Pass True, if the user can send text messages, contacts, locations and venues + :param can_send_media_messages: Pass True, if the user can send audios, documents, photos, videos, video notes + and voice notes, implies can_send_messages + :param can_send_polls: Pass True, if the user is allowed to send polls, implies can_send_messages + :param can_send_other_messages: Pass True, if the user can send animations, games, stickers and use inline bots, implies can_send_media_messages + :param can_add_web_page_previews: Pass True, if the user may add web page previews to their messages, + implies can_send_media_messages + :param can_change_info: Pass True, if the user is allowed to change the chat title, photo and other settings. + Ignored in public supergroups + :param can_invite_users: Pass True, if the user is allowed to invite new users to the chat, + implies can_invite_users + :param can_pin_messages: Pass True, if the user is allowed to pin messages. Ignored in public supergroups + :return: True on success + """ + return apihelper.restrict_chat_member( + self.token, chat_id, user_id, until_date, + can_send_messages, can_send_media_messages, + can_send_polls, can_send_other_messages, + can_add_web_page_previews, can_change_info, + can_invite_users, can_pin_messages) + + def promote_chat_member( + self, chat_id: Union[int, str], user_id: int, + can_change_info: Optional[bool]=None, + can_post_messages: Optional[bool]=None, + can_edit_messages: Optional[bool]=None, + can_delete_messages: Optional[bool]=None, + can_invite_users: Optional[bool]=None, + can_restrict_members: Optional[bool]=None, + can_pin_messages: Optional[bool]=None, + can_promote_members: Optional[bool]=None, + is_anonymous: Optional[bool]=None, + can_manage_chat: Optional[bool]=None, + can_manage_voice_chats: Optional[bool]=None) -> bool: + """ + Use this method to promote or demote a user in a supergroup or a channel. The bot must be an administrator + in the chat for this to work and must have the appropriate admin rights. + Pass False for all boolean parameters to demote a user. + + Telegram documentation: https://core.telegram.org/bots/api#promotechatmember + + :param chat_id: Unique identifier for the target chat or username of the target channel ( + in the format @channelusername) + :param user_id: Int : Unique identifier of the target user + :param can_change_info: Bool: Pass True, if the administrator can change chat title, photo and other settings + :param can_post_messages: Bool : Pass True, if the administrator can create channel posts, channels only + :param can_edit_messages: Bool : Pass True, if the administrator can edit messages of other users, channels only + :param can_delete_messages: Bool : Pass True, if the administrator can delete messages of other users + :param can_invite_users: Bool : Pass True, if the administrator can invite new users to the chat + :param can_restrict_members: Bool: Pass True, if the administrator can restrict, ban or unban chat members + :param can_pin_messages: Bool: Pass True, if the administrator can pin messages, supergroups only + :param can_promote_members: Bool: Pass True, if the administrator can add new administrators with a subset + of his own privileges or demote administrators that he has promoted, directly or indirectly + (promoted by administrators that were appointed by him) + :param is_anonymous: Bool: Pass True, if the administrator's presence in the chat is hidden + :param can_manage_chat: Bool: Pass True, if the administrator can access the chat event log, chat statistics, + message statistics in channels, see channel members, + see anonymous administrators in supergroups and ignore slow mode. + Implied by any other administrator privilege + :param can_manage_voice_chats: Bool: Pass True, if the administrator can manage voice chats + For now, bots can use this privilege only for passing to other administrators. + :return: True on success. + """ + return apihelper.promote_chat_member( + self.token, chat_id, user_id, can_change_info, can_post_messages, + can_edit_messages, can_delete_messages, can_invite_users, + can_restrict_members, can_pin_messages, can_promote_members, + is_anonymous, can_manage_chat, can_manage_voice_chats) + + def set_chat_administrator_custom_title( + self, chat_id: Union[int, str], user_id: int, custom_title: str) -> bool: + """ + Use this method to set a custom title for an administrator + in a supergroup promoted by the bot. + + Telegram documentation: https://core.telegram.org/bots/api#setchatadministratorcustomtitle + + :param chat_id: Unique identifier for the target chat or username of the target supergroup + (in the format @supergroupusername) + :param user_id: Unique identifier of the target user + :param custom_title: New custom title for the administrator; + 0-16 characters, emoji are not allowed + :return: True on success. + """ + return apihelper.set_chat_administrator_custom_title(self.token, chat_id, user_id, custom_title) + + def ban_chat_sender_chat(self, chat_id: Union[int, str], sender_chat_id: Union[int, str]) -> bool: + """ + Use this method to ban a channel chat in a supergroup or a channel. + The owner of the chat will not be able to send messages and join live + streams on behalf of the chat, unless it is unbanned first. + The bot must be an administrator in the supergroup or channel + for this to work and must have the appropriate administrator rights. + Returns True on success. + + Telegram documentation: https://core.telegram.org/bots/api#banchatsenderchat + + :param chat_id: Unique identifier for the target chat or username of the target channel (in the format @channelusername) + :param sender_chat_id: Unique identifier of the target sender chat + :return: True on success. + """ + return apihelper.ban_chat_sender_chat(self.token, chat_id, sender_chat_id) + + def unban_chat_sender_chat(self, chat_id: Union[int, str], sender_chat_id: Union[int, str]) -> bool: + """ + Use this method to unban a previously banned channel chat in a supergroup or channel. + The bot must be an administrator for this to work and must have the appropriate + administrator rights. + Returns True on success. + + Telegram documentation: https://core.telegram.org/bots/api#unbanchatsenderchat + + :params: + :param chat_id: Unique identifier for the target chat or username of the target channel (in the format @channelusername) + :param sender_chat_id: Unique identifier of the target sender chat + :return: True on success. + """ + return apihelper.unban_chat_sender_chat(self.token, chat_id, sender_chat_id) + + def set_chat_permissions( + self, chat_id: Union[int, str], permissions: types.ChatPermissions) -> bool: + """ + Use this method to set default chat permissions for all members. + The bot must be an administrator in the group or a supergroup for this to work + and must have the can_restrict_members admin rights. + + Telegram documentation: https://core.telegram.org/bots/api#setchatpermissions + + :param chat_id: Unique identifier for the target chat or username of the target supergroup + (in the format @supergroupusername) + :param permissions: New default chat permissions + :return: True on success + """ + return apihelper.set_chat_permissions(self.token, chat_id, permissions) + + def create_chat_invite_link( + self, chat_id: Union[int, str], + name: Optional[str]=None, + expire_date: Optional[Union[int, datetime]]=None, + member_limit: Optional[int]=None, + creates_join_request: Optional[bool]=None) -> types.ChatInviteLink: + """ + Use this method to create an additional invite link for a chat. + The bot must be an administrator in the chat for this to work and must have the appropriate admin rights. + + Telegram documentation: https://core.telegram.org/bots/api#createchatinvitelink + + :param chat_id: Id: Unique identifier for the target chat or username of the target channel + (in the format @channelusername) + :param name: Invite link name; 0-32 characters + :param expire_date: Point in time (Unix timestamp) when the link will expire + :param member_limit: Maximum number of users that can be members of the chat simultaneously + :param creates_join_request: True, if users joining the chat via the link need to be approved by chat administrators. If True, member_limit can't be specified + :return: + """ + return types.ChatInviteLink.de_json( + apihelper.create_chat_invite_link(self.token, chat_id, name, expire_date, member_limit, creates_join_request) + ) + + def edit_chat_invite_link( + self, chat_id: Union[int, str], + invite_link: Optional[str] = None, + name: Optional[str]=None, + expire_date: Optional[Union[int, datetime]]=None, + member_limit: Optional[int]=None, + creates_join_request: Optional[bool]=None) -> types.ChatInviteLink: + """ + Use this method to edit a non-primary invite link created by the bot. + The bot must be an administrator in the chat for this to work and must have the appropriate admin rights. + + Telegram documentation: https://core.telegram.org/bots/api#editchatinvitelink + + :param chat_id: Id: Unique identifier for the target chat or username of the target channel + (in the format @channelusername) + :param name: Invite link name; 0-32 characters + :param invite_link: The invite link to edit + :param expire_date: Point in time (Unix timestamp) when the link will expire + :param member_limit: Maximum number of users that can be members of the chat simultaneously + :param creates_join_request: True, if users joining the chat via the link need to be approved by chat administrators. If True, member_limit can't be specified + :return: + """ + return types.ChatInviteLink.de_json( + apihelper.edit_chat_invite_link(self.token, chat_id, name, invite_link, expire_date, member_limit, creates_join_request) + ) + + def revoke_chat_invite_link( + self, chat_id: Union[int, str], invite_link: str) -> types.ChatInviteLink: + """ + Use this method to revoke an invite link created by the bot. + Note: If the primary link is revoked, a new link is automatically generated The bot must be an administrator + in the chat for this to work and must have the appropriate admin rights. + + Telegram documentation: https://core.telegram.org/bots/api#revokechatinvitelink + + :param chat_id: Id: Unique identifier for the target chat or username of the target channel + (in the format @channelusername) + :param invite_link: The invite link to revoke + :return: + """ + return types.ChatInviteLink.de_json( + apihelper.revoke_chat_invite_link(self.token, chat_id, invite_link) + ) + + def export_chat_invite_link(self, chat_id: Union[int, str]) -> str: + """ + Use this method to export an invite link to a supergroup or a channel. The bot must be an administrator + in the chat for this to work and must have the appropriate admin rights. + + Telegram documentation: https://core.telegram.org/bots/api#exportchatinvitelink + + :param chat_id: Id: Unique identifier for the target chat or username of the target channel + (in the format @channelusername) + :return: exported invite link as String on success. + """ + return apihelper.export_chat_invite_link(self.token, chat_id) + + def approve_chat_join_request(self, chat_id: Union[str, int], user_id: Union[int, str]) -> bool: + """ + Use this method to approve a chat join request. + The bot must be an administrator in the chat for this to work and must have + the can_invite_users administrator right. Returns True on success. + + Telegram documentation: https://core.telegram.org/bots/api#approvechatjoinrequest + + :param chat_id: Unique identifier for the target chat or username of the target supergroup + (in the format @supergroupusername) + :param user_id: Unique identifier of the target user + :return: True on success. + """ + return apihelper.approve_chat_join_request(self.token, chat_id, user_id) + + def decline_chat_join_request(self, chat_id: Union[str, int], user_id: Union[int, str]) -> bool: + """ + Use this method to decline a chat join request. + The bot must be an administrator in the chat for this to work and must have + the can_invite_users administrator right. Returns True on success. + + Telegram documentation: https://core.telegram.org/bots/api#declinechatjoinrequest + + :param chat_id: Unique identifier for the target chat or username of the target supergroup + (in the format @supergroupusername) + :param user_id: Unique identifier of the target user + :return: True on success. + """ + return apihelper.decline_chat_join_request(self.token, chat_id, user_id) + + def set_chat_photo(self, chat_id: Union[int, str], photo: Any) -> bool: + """ + Use this method to set a new profile photo for the chat. Photos can't be changed for private chats. + The bot must be an administrator in the chat for this to work and must have the appropriate admin rights. + Returns True on success. + Note: In regular groups (non-supergroups), this method will only work if the ‘All Members Are Admins’ + setting is off in the target group. + + Telegram documentation: https://core.telegram.org/bots/api#setchatphoto + + :param chat_id: Int or Str: Unique identifier for the target chat or username of the target channel + (in the format @channelusername) + :param photo: InputFile: New chat photo, uploaded using multipart/form-data + :return: + """ + return apihelper.set_chat_photo(self.token, chat_id, photo) + + def delete_chat_photo(self, chat_id: Union[int, str]) -> bool: + """ + Use this method to delete a chat photo. Photos can't be changed for private chats. + The bot must be an administrator in the chat for this to work and must have the appropriate admin rights. + Returns True on success. + Note: In regular groups (non-supergroups), this method will only work if the ‘All Members Are Admins’ setting is off in the target group. + + Telegram documentation: https://core.telegram.org/bots/api#deletechatphoto + + :param chat_id: Int or Str: Unique identifier for the target chat or username of the target channel + (in the format @channelusername) + """ + return apihelper.delete_chat_photo(self.token, chat_id) + + def get_my_commands(self, scope: Optional[types.BotCommandScope]=None, + language_code: Optional[str]=None) -> List[types.BotCommand]: + """ + Use this method to get the current list of the bot's commands. + Returns List of BotCommand on success. + + Telegram documentation: https://core.telegram.org/bots/api#getmycommands + + :param scope: The scope of users for which the commands are relevant. + Defaults to BotCommandScopeDefault. + :param language_code: A two-letter ISO 639-1 language code. If empty, + commands will be applied to all users from the given scope, + for whose language there are no dedicated commands + """ + result = apihelper.get_my_commands(self.token, scope, language_code) + return [types.BotCommand.de_json(cmd) for cmd in result] + + def set_my_commands(self, commands: List[types.BotCommand], + scope: Optional[types.BotCommandScope]=None, + language_code: Optional[str]=None) -> bool: + """ + Use this method to change the list of the bot's commands. + + Telegram documentation: https://core.telegram.org/bots/api#setmycommands + + :param commands: List of BotCommand. At most 100 commands can be specified. + :param scope: The scope of users for which the commands are relevant. + Defaults to BotCommandScopeDefault. + :param language_code: A two-letter ISO 639-1 language code. If empty, + commands will be applied to all users from the given scope, + for whose language there are no dedicated commands + :return: + """ + return apihelper.set_my_commands(self.token, commands, scope, language_code) + + def delete_my_commands(self, scope: Optional[types.BotCommandScope]=None, + language_code: Optional[str]=None) -> bool: + """ + Use this method to delete the list of the bot's commands for the given scope and user language. + After deletion, higher level commands will be shown to affected users. + Returns True on success. + + Telegram documentation: https://core.telegram.org/bots/api#deletemycommands + + :param scope: The scope of users for which the commands are relevant. + Defaults to BotCommandScopeDefault. + :param language_code: A two-letter ISO 639-1 language code. If empty, + commands will be applied to all users from the given scope, + for whose language there are no dedicated commands + """ + return apihelper.delete_my_commands(self.token, scope, language_code) + + def set_chat_title(self, chat_id: Union[int, str], title: str) -> bool: + """ + Use this method to change the title of a chat. Titles can't be changed for private chats. + The bot must be an administrator in the chat for this to work and must have the appropriate admin rights. + Returns True on success. + Note: In regular groups (non-supergroups), this method will only work if the ‘All Members Are Admins’ + setting is off in the target group. + + Telegram documentation: https://core.telegram.org/bots/api#setchattitle + + :param chat_id: Int or Str: Unique identifier for the target chat or username of the target channel + (in the format @channelusername) + :param title: New chat title, 1-255 characters + :return: + """ + return apihelper.set_chat_title(self.token, chat_id, title) + + def set_chat_description(self, chat_id: Union[int, str], description: Optional[str]=None) -> bool: + """ + Use this method to change the description of a supergroup or a channel. + The bot must be an administrator in the chat for this to work and must have the appropriate admin rights. + + Telegram documentation: https://core.telegram.org/bots/api#setchatdescription + + :param chat_id: Int or Str: Unique identifier for the target chat or username of the target channel + (in the format @channelusername) + :param description: Str: New chat description, 0-255 characters + :return: True on success. + """ + return apihelper.set_chat_description(self.token, chat_id, description) + + def pin_chat_message( + self, chat_id: Union[int, str], message_id: int, + disable_notification: Optional[bool]=False) -> bool: + """ + Use this method to pin a message in a supergroup. + The bot must be an administrator in the chat for this to work and must have the appropriate admin rights. + Returns True on success. + + Telegram documentation: https://core.telegram.org/bots/api#pinchatmessage + + :param chat_id: Int or Str: Unique identifier for the target chat or username of the target channel + (in the format @channelusername) + :param message_id: Int: Identifier of a message to pin + :param disable_notification: Bool: Pass True, if it is not necessary to send a notification + to all group members about the new pinned message + :return: + """ + return apihelper.pin_chat_message(self.token, chat_id, message_id, disable_notification) + + def unpin_chat_message(self, chat_id: Union[int, str], message_id: Optional[int]=None) -> bool: + """ + Use this method to unpin specific pinned message in a supergroup chat. + The bot must be an administrator in the chat for this to work and must have the appropriate admin rights. + Returns True on success. + + Telegram documentation: https://core.telegram.org/bots/api#unpinchatmessage + + :param chat_id: Int or Str: Unique identifier for the target chat or username of the target channel + (in the format @channelusername) + :param message_id: Int: Identifier of a message to unpin + :return: + """ + return apihelper.unpin_chat_message(self.token, chat_id, message_id) + + def unpin_all_chat_messages(self, chat_id: Union[int, str]) -> bool: + """ + Use this method to unpin a all pinned messages in a supergroup chat. + The bot must be an administrator in the chat for this to work and must have the appropriate admin rights. + Returns True on success. + + Telegram documentation: https://core.telegram.org/bots/api#unpinallchatmessages + + :param chat_id: Int or Str: Unique identifier for the target chat or username of the target channel + (in the format @channelusername) + :return: + """ + return apihelper.unpin_all_chat_messages(self.token, chat_id) + + def edit_message_text( + self, text: str, + chat_id: Optional[Union[int, str]]=None, + message_id: Optional[int]=None, + inline_message_id: Optional[str]=None, + parse_mode: Optional[str]=None, + entities: Optional[List[types.MessageEntity]]=None, + disable_web_page_preview: Optional[bool]=None, + reply_markup: Optional[REPLY_MARKUP_TYPES]=None) -> Union[types.Message, bool]: + """ + Use this method to edit text and game messages. + + Telegram documentation: https://core.telegram.org/bots/api#editmessagetext + + :param text: + :param chat_id: + :param message_id: + :param inline_message_id: + :param parse_mode: + :param entities: + :param disable_web_page_preview: + :param reply_markup: + :return: + """ + parse_mode = self.parse_mode if (parse_mode is None) else parse_mode + + result = apihelper.edit_message_text(self.token, text, chat_id, message_id, inline_message_id, parse_mode, + entities, disable_web_page_preview, reply_markup) + if type(result) == bool: # if edit inline message return is bool not Message. + return result + return types.Message.de_json(result) + + def edit_message_media( + self, media: Any, chat_id: Optional[Union[int, str]]=None, + message_id: Optional[int]=None, + inline_message_id: Optional[str]=None, + reply_markup: Optional[REPLY_MARKUP_TYPES]=None) -> Union[types.Message, bool]: + """ + Use this method to edit animation, audio, document, photo, or video messages. + If a message is a part of a message album, then it can be edited only to a photo or a video. + Otherwise, message type can be changed arbitrarily. When inline message is edited, new file can't be uploaded. + Use previously uploaded file via its file_id or specify a URL. + + Telegram documentation: https://core.telegram.org/bots/api#editmessagemedia + + :param media: + :param chat_id: + :param message_id: + :param inline_message_id: + :param reply_markup: + :return: + """ + result = apihelper.edit_message_media(self.token, media, chat_id, message_id, inline_message_id, reply_markup) + if type(result) == bool: # if edit inline message return is bool not Message. + return result + return types.Message.de_json(result) + + def edit_message_reply_markup( + self, chat_id: Optional[Union[int, str]]=None, + message_id: Optional[int]=None, + inline_message_id: Optional[str]=None, + reply_markup: Optional[REPLY_MARKUP_TYPES]=None) -> Union[types.Message, bool]: + """ + Use this method to edit only the reply markup of messages. + + Telegram documentation: https://core.telegram.org/bots/api#editmessagereplymarkup + + :param chat_id: + :param message_id: + :param inline_message_id: + :param reply_markup: + :return: + """ + result = apihelper.edit_message_reply_markup(self.token, chat_id, message_id, inline_message_id, reply_markup) + if type(result) == bool: + return result + return types.Message.de_json(result) + + def send_game( + self, chat_id: Union[int, str], game_short_name: str, + disable_notification: Optional[bool]=None, + reply_to_message_id: Optional[int]=None, + reply_markup: Optional[REPLY_MARKUP_TYPES]=None, + timeout: Optional[int]=None, + allow_sending_without_reply: Optional[bool]=None, + protect_content: Optional[bool]=None) -> types.Message: + """ + Used to send the game. + + Telegram documentation: https://core.telegram.org/bots/api#sendgame + + :param chat_id: + :param game_short_name: + :param disable_notification: + :param reply_to_message_id: + :param reply_markup: + :param timeout: + :param allow_sending_without_reply: + :param protect_content: + :return: + """ + result = apihelper.send_game( + self.token, chat_id, game_short_name, disable_notification, + reply_to_message_id, reply_markup, timeout, + allow_sending_without_reply, protect_content) + return types.Message.de_json(result) + + def set_game_score( + self, user_id: Union[int, str], score: int, + force: Optional[bool]=None, + chat_id: Optional[Union[int, str]]=None, + message_id: Optional[int]=None, + inline_message_id: Optional[str]=None, + disable_edit_message: Optional[bool]=None) -> Union[types.Message, bool]: + """ + Sets the value of points in the game to a specific user. + + Telegram documentation: https://core.telegram.org/bots/api#setgamecore + + :param user_id: + :param score: + :param force: + :param chat_id: + :param message_id: + :param inline_message_id: + :param disable_edit_message: + :return: + """ + result = apihelper.set_game_score(self.token, user_id, score, force, disable_edit_message, chat_id, + message_id, inline_message_id) + if type(result) == bool: + return result + return types.Message.de_json(result) + + def get_game_high_scores( + self, user_id: int, chat_id: Optional[Union[int, str]]=None, + message_id: Optional[int]=None, + inline_message_id: Optional[str]=None) -> List[types.GameHighScore]: + """ + Gets top points and game play. + + Telegram documentation: https://core.telegram.org/bots/api#getgamehighscores + + :param user_id: + :param chat_id: + :param message_id: + :param inline_message_id: + :return: + """ + result = apihelper.get_game_high_scores(self.token, user_id, chat_id, message_id, inline_message_id) + return [types.GameHighScore.de_json(r) for r in result] + + # TODO: rewrite this method like in API + def send_invoice( + self, chat_id: Union[int, str], title: str, description: str, + invoice_payload: str, provider_token: str, currency: str, + prices: List[types.LabeledPrice], start_parameter: Optional[str]=None, + photo_url: Optional[str]=None, photo_size: Optional[int]=None, + photo_width: Optional[int]=None, photo_height: Optional[int]=None, + need_name: Optional[bool]=None, need_phone_number: Optional[bool]=None, + need_email: Optional[bool]=None, need_shipping_address: Optional[bool]=None, + send_phone_number_to_provider: Optional[bool]=None, + send_email_to_provider: Optional[bool]=None, + is_flexible: Optional[bool]=None, + disable_notification: Optional[bool]=None, + reply_to_message_id: Optional[int]=None, + reply_markup: Optional[REPLY_MARKUP_TYPES]=None, + provider_data: Optional[str]=None, + timeout: Optional[int]=None, + allow_sending_without_reply: Optional[bool]=None, + max_tip_amount: Optional[int] = None, + suggested_tip_amounts: Optional[List[int]]=None, + protect_content: Optional[bool]=None) -> types.Message: + """ + Sends invoice. + + Telegram documentation: https://core.telegram.org/bots/api#sendinvoice + + :param chat_id: Unique identifier for the target private chat + :param title: Product name + :param description: Product description + :param invoice_payload: Bot-defined invoice payload, 1-128 bytes. This will not be displayed to the user, + use for your internal processes. + :param provider_token: Payments provider token, obtained via @Botfather + :param currency: Three-letter ISO 4217 currency code, + see https://core.telegram.org/bots/payments#supported-currencies + :param prices: Price breakdown, a list of components + (e.g. product price, tax, discount, delivery cost, delivery tax, bonus, etc.) + :param start_parameter: Unique deep-linking parameter that can be used to generate this invoice + when used as a start parameter + :param photo_url: URL of the product photo for the invoice. Can be a photo of the goods + or a marketing image for a service. People like it better when they see what they are paying for. + :param photo_size: Photo size + :param photo_width: Photo width + :param photo_height: Photo height + :param need_name: Pass True, if you require the user's full name to complete the order + :param need_phone_number: Pass True, if you require the user's phone number to complete the order + :param need_email: Pass True, if you require the user's email to complete the order + :param need_shipping_address: Pass True, if you require the user's shipping address to complete the order + :param is_flexible: Pass True, if the final price depends on the shipping method + :param send_phone_number_to_provider: Pass True, if user's phone number should be sent to provider + :param send_email_to_provider: Pass True, if user's email address should be sent to provider + :param disable_notification: Sends the message silently. Users will receive a notification with no sound. + :param reply_to_message_id: If the message is a reply, ID of the original message + :param reply_markup: A JSON-serialized object for an inline keyboard. If empty, + one 'Pay total price' button will be shown. If not empty, the first button must be a Pay button + :param provider_data: A JSON-serialized data about the invoice, which will be shared with the payment provider. + A detailed description of required fields should be provided by the payment provider. + :param timeout: + :param allow_sending_without_reply: + :param max_tip_amount: The maximum accepted amount for tips in the smallest units of the currency + :param suggested_tip_amounts: A JSON-serialized array of suggested amounts of tips in the smallest + units of the currency. At most 4 suggested tip amounts can be specified. The suggested tip + amounts must be positive, passed in a strictly increased order and must not exceed max_tip_amount. + :param protect_content: + :return: + """ + result = apihelper.send_invoice( + self.token, chat_id, title, description, invoice_payload, provider_token, + currency, prices, start_parameter, photo_url, photo_size, photo_width, + photo_height, need_name, need_phone_number, need_email, need_shipping_address, + send_phone_number_to_provider, send_email_to_provider, is_flexible, disable_notification, + reply_to_message_id, reply_markup, provider_data, timeout, allow_sending_without_reply, + max_tip_amount, suggested_tip_amounts, protect_content) + return types.Message.de_json(result) + + # noinspection PyShadowingBuiltins + # TODO: rewrite this method like in API + def send_poll( + self, chat_id: Union[int, str], question: str, options: List[str], + is_anonymous: Optional[bool]=None, type: Optional[str]=None, + allows_multiple_answers: Optional[bool]=None, + correct_option_id: Optional[int]=None, + explanation: Optional[str]=None, + explanation_parse_mode: Optional[str]=None, + open_period: Optional[int]=None, + close_date: Optional[Union[int, datetime]]=None, + is_closed: Optional[bool]=None, + disable_notification: Optional[bool]=False, + reply_to_message_id: Optional[int]=None, + reply_markup: Optional[REPLY_MARKUP_TYPES]=None, + allow_sending_without_reply: Optional[bool]=None, + timeout: Optional[int]=None, + explanation_entities: Optional[List[types.MessageEntity]]=None, + protect_content: Optional[bool]=None) -> types.Message: + """ + Sends a poll. + + Telegram documentation: https://core.telegram.org/bots/api#sendpoll + + :param chat_id: + :param question: + :param options: array of str with answers + :param is_anonymous: + :param type: + :param allows_multiple_answers: + :param correct_option_id: + :param explanation: + :param explanation_parse_mode: + :param open_period: + :param close_date: + :param is_closed: + :param disable_notification: + :param reply_to_message_id: + :param allow_sending_without_reply: + :param reply_markup: + :param timeout: + :param explanation_entities: + :param protect_content: + :return: + """ + if isinstance(question, types.Poll): + raise RuntimeError("The send_poll signature was changed, please see send_poll function details.") + + return types.Message.de_json( + apihelper.send_poll( + self.token, chat_id, + question, options, + is_anonymous, type, allows_multiple_answers, correct_option_id, + explanation, explanation_parse_mode, open_period, close_date, is_closed, + disable_notification, reply_to_message_id, allow_sending_without_reply, + reply_markup, timeout, explanation_entities, protect_content)) + + def stop_poll( + self, chat_id: Union[int, str], message_id: int, + reply_markup: Optional[REPLY_MARKUP_TYPES]=None) -> types.Poll: + """ + Stops a poll. + + Telegram documentation: https://core.telegram.org/bots/api#stoppoll + + :param chat_id: + :param message_id: + :param reply_markup: + :return: + """ + return types.Poll.de_json(apihelper.stop_poll(self.token, chat_id, message_id, reply_markup)) + + def answer_shipping_query( + self, shipping_query_id: str, ok: bool, + shipping_options: Optional[List[types.ShippingOption]]=None, + error_message: Optional[str]=None) -> bool: + """ + Asks for an answer to a shipping question. + + Telegram documentation: https://core.telegram.org/bots/api#answershippingquery + + :param shipping_query_id: + :param ok: + :param shipping_options: + :param error_message: + :return: + """ + return apihelper.answer_shipping_query(self.token, shipping_query_id, ok, shipping_options, error_message) + + def answer_pre_checkout_query( + self, pre_checkout_query_id: int, ok: bool, + error_message: Optional[str]=None) -> bool: + """ + Response to a request for pre-inspection. + + Telegram documentation: https://core.telegram.org/bots/api#answerprecheckoutquery + + :param pre_checkout_query_id: + :param ok: + :param error_message: + :return: + """ + return apihelper.answer_pre_checkout_query(self.token, pre_checkout_query_id, ok, error_message) + + def edit_message_caption( + self, caption: str, chat_id: Optional[Union[int, str]]=None, + message_id: Optional[int]=None, + inline_message_id: Optional[str]=None, + parse_mode: Optional[str]=None, + caption_entities: Optional[List[types.MessageEntity]]=None, + reply_markup: Optional[REPLY_MARKUP_TYPES]=None) -> Union[types.Message, bool]: + """ + Use this method to edit captions of messages. + + Telegram documentation: https://core.telegram.org/bots/api#editmessagecaption + + :param caption: + :param chat_id: + :param message_id: + :param inline_message_id: + :param parse_mode: + :param caption_entities: + :param reply_markup: + :return: + """ + parse_mode = self.parse_mode if (parse_mode is None) else parse_mode + + result = apihelper.edit_message_caption(self.token, caption, chat_id, message_id, inline_message_id, + parse_mode, caption_entities, reply_markup) + if type(result) == bool: + return result + return types.Message.de_json(result) + + def reply_to(self, message: types.Message, text: str, **kwargs) -> types.Message: + """ + Convenience function for `send_message(message.chat.id, text, reply_to_message_id=message.message_id, **kwargs)` + + :param message: + :param text: + :param kwargs: + :return: + """ + return self.send_message(message.chat.id, text, reply_to_message_id=message.message_id, **kwargs) + + def answer_inline_query( + self, inline_query_id: str, + results: List[Any], + cache_time: Optional[int]=None, + is_personal: Optional[bool]=None, + next_offset: Optional[str]=None, + switch_pm_text: Optional[str]=None, + switch_pm_parameter: Optional[str]=None) -> bool: + """ + Use this method to send answers to an inline query. On success, True is returned. + No more than 50 results per query are allowed. + + Telegram documentation: https://core.telegram.org/bots/api#answerinlinequery + + :param inline_query_id: Unique identifier for the answered query + :param results: Array of results for the inline query + :param cache_time: The maximum amount of time in seconds that the result of the inline query + may be cached on the server. + :param is_personal: Pass True, if results may be cached on the server side only for + the user that sent the query. + :param next_offset: Pass the offset that a client should send in the next query with the same text + to receive more results. + :param switch_pm_parameter: If passed, clients will display a button with specified text that switches the user + to a private chat with the bot and sends the bot a start message with the parameter switch_pm_parameter + :param switch_pm_text: Parameter for the start message sent to the bot when user presses the switch button + :return: True means success. + """ + return apihelper.answer_inline_query(self.token, inline_query_id, results, cache_time, is_personal, next_offset, + switch_pm_text, switch_pm_parameter) + + def answer_callback_query( + self, callback_query_id: int, + text: Optional[str]=None, show_alert: Optional[bool]=None, + url: Optional[str]=None, cache_time: Optional[int]=None) -> bool: + """ + Use this method to send answers to callback queries sent from inline keyboards. The answer will be displayed to + the user as a notification at the top of the chat screen or as an alert. + + Telegram documentation: https://core.telegram.org/bots/api#answercallbackquery + + :param callback_query_id: + :param text: + :param show_alert: + :param url: + :param cache_time: + :return: + """ + return apihelper.answer_callback_query(self.token, callback_query_id, text, show_alert, url, cache_time) + + def set_sticker_set_thumb( + self, name: str, user_id: int, thumb: Union[Any, str]=None): + """ + Use this method to set the thumbnail of a sticker set. + Animated thumbnails can be set for animated sticker sets only. Returns True on success. + + Telegram documentation: https://core.telegram.org/bots/api#setstickersetthumb + + :param name: Sticker set name + :param user_id: User identifier + :param thumb: + """ + return apihelper.set_sticker_set_thumb(self.token, name, user_id, thumb) + + def get_sticker_set(self, name: str) -> types.StickerSet: + """ + Use this method to get a sticker set. On success, a StickerSet object is returned. + + Telegram documentation: https://core.telegram.org/bots/api#getstickerset + + :param name: + :return: + """ + result = apihelper.get_sticker_set(self.token, name) + return types.StickerSet.de_json(result) + + def upload_sticker_file(self, user_id: int, png_sticker: Union[Any, str]) -> types.File: + """ + Use this method to upload a .png file with a sticker for later use in createNewStickerSet and addStickerToSet + methods (can be used multiple times). Returns the uploaded File on success. + + Telegram documentation: https://core.telegram.org/bots/api#uploadstickerfile + + :param user_id: + :param png_sticker: + :return: + """ + result = apihelper.upload_sticker_file(self.token, user_id, png_sticker) + return types.File.de_json(result) + + def create_new_sticker_set( + self, user_id: int, name: str, title: str, + emojis: str, + png_sticker: Union[Any, str]=None, + tgs_sticker: Union[Any, str]=None, + webm_sticker: Union[Any, str]=None, + contains_masks: Optional[bool]=None, + mask_position: Optional[types.MaskPosition]=None) -> bool: + """ + Use this method to create new sticker set owned by a user. + The bot will be able to edit the created sticker set. + Returns True on success. + + Telegram documentation: https://core.telegram.org/bots/api#createnewstickerset + + :param user_id: + :param name: + :param title: + :param emojis: + :param png_sticker: + :param tgs_sticker: + :param webm_sticker: + :param contains_masks: + :param mask_position: + :return: + """ + return apihelper.create_new_sticker_set( + self.token, user_id, name, title, emojis, png_sticker, tgs_sticker, + contains_masks, mask_position, webm_sticker) + + def add_sticker_to_set( + self, user_id: int, name: str, emojis: str, + png_sticker: Optional[Union[Any, str]]=None, + tgs_sticker: Optional[Union[Any, str]]=None, + webm_sticker: Optional[Union[Any, str]]=None, + mask_position: Optional[types.MaskPosition]=None) -> bool: + """ + Use this method to add a new sticker to a set created by the bot. + It's required to pass `png_sticker` or `tgs_sticker`. + Returns True on success. + + Telegram documentation: https://core.telegram.org/bots/api#addstickertoset + + :param user_id: + :param name: + :param emojis: + :param png_sticker: Required if `tgs_sticker` is None + :param tgs_sticker: Required if `png_sticker` is None + :param webm_sticker: + :param mask_position: + :return: + """ + return apihelper.add_sticker_to_set( + self.token, user_id, name, emojis, png_sticker, tgs_sticker, mask_position, webm_sticker) + + def set_sticker_position_in_set(self, sticker: str, position: int) -> bool: + """ + Use this method to move a sticker in a set created by the bot to a specific position . Returns True on success. + + Telegram documentation: https://core.telegram.org/bots/api#setstickerpositioninset + + :param sticker: + :param position: + :return: + """ + return apihelper.set_sticker_position_in_set(self.token, sticker, position) + + def delete_sticker_from_set(self, sticker: str) -> bool: + """ + Use this method to delete a sticker from a set created by the bot. Returns True on success. + + Telegram documentation: https://core.telegram.org/bots/api#deletestickerfromset + + :param sticker: + :return: + """ + return apihelper.delete_sticker_from_set(self.token, sticker) + + def register_for_reply( + self, message: types.Message, callback: Callable, *args, **kwargs) -> None: + """ + Registers a callback function to be notified when a reply to `message` arrives. + + Warning: In case `callback` as lambda function, saving reply handlers will not work. + + :param message: The message for which we are awaiting a reply. + :param callback: The callback function to be called when a reply arrives. Must accept one `message` + parameter, which will contain the replied message. + """ + message_id = message.message_id + self.register_for_reply_by_message_id(message_id, callback, *args, **kwargs) + + def register_for_reply_by_message_id( + self, message_id: int, callback: Callable, *args, **kwargs) -> None: + """ + Registers a callback function to be notified when a reply to `message` arrives. + + Warning: In case `callback` as lambda function, saving reply handlers will not work. + + :param message_id: The id of the message for which we are awaiting a reply. + :param callback: The callback function to be called when a reply arrives. Must accept one `message` + parameter, which will contain the replied message. + """ + self.reply_backend.register_handler(message_id, Handler(callback, *args, **kwargs)) + + def _notify_reply_handlers(self, new_messages) -> None: + """ + Notify handlers of the answers + + :param new_messages: + :return: + """ + for message in new_messages: + if hasattr(message, "reply_to_message") and message.reply_to_message is not None: + handlers = self.reply_backend.get_handlers(message.reply_to_message.message_id) + if handlers: + for handler in handlers: + self._exec_task(handler["callback"], message, *handler["args"], **handler["kwargs"]) + + def register_next_step_handler( + self, message: types.Message, callback: Callable, *args, **kwargs) -> None: + """ + Registers a callback function to be notified when new message arrives after `message`. + + Warning: In case `callback` as lambda function, saving next step handlers will not work. + + :param message: The message for which we want to handle new message in the same chat. + :param callback: The callback function which next new message arrives. + :param args: Args to pass in callback func + :param kwargs: Args to pass in callback func + """ + chat_id = message.chat.id + self.register_next_step_handler_by_chat_id(chat_id, callback, *args, **kwargs) + + + def setup_middleware(self, middleware: BaseMiddleware): + """ + Register middleware + + :param middleware: Subclass of `telebot.handler_backends.BaseMiddleware` + :return: None + """ + if not self.use_class_middlewares: + logger.warning('Middleware is not enabled. Pass use_class_middlewares=True to enable it.') + return + self.middlewares.append(middleware) + + + + def set_state(self, user_id: int, state: Union[int, str], chat_id: int=None) -> None: + """ + Sets a new state of a user. + + :param user_id: + :param state: new state. can be string or integer. + :param chat_id: + """ + if chat_id is None: + chat_id = user_id + self.current_states.set_state(chat_id, user_id, state) + + def reset_data(self, user_id: int, chat_id: int=None): + """ + Reset data for a user in chat. + + :param user_id: + :param chat_id: + """ + if chat_id is None: + chat_id = user_id + + self.current_states.reset_data(chat_id, user_id) + def delete_state(self, user_id: int, chat_id: int=None) -> None: + """ + Delete the current state of a user. + + :param user_id: + :param chat_id: + :return: + """ + if chat_id is None: + chat_id = user_id + self.current_states.delete_state(chat_id, user_id) + + def retrieve_data(self, user_id: int, chat_id: int=None) -> Optional[Union[int, str]]: + if chat_id is None: + chat_id = user_id + return self.current_states.get_interactive_data(chat_id, user_id) + + def get_state(self, user_id: int, chat_id: int=None) -> Optional[Union[int, str]]: + """ + Get current state of a user. + + :param user_id: + :param chat_id: + :return: state of a user + """ + if chat_id is None: + chat_id = user_id + return self.current_states.get_state(chat_id, user_id) + + def add_data(self, user_id: int, chat_id:int=None, **kwargs): + """ + Add data to states. + + :param user_id: + :param chat_id: + """ + if chat_id is None: + chat_id = user_id + for key, value in kwargs.items(): + self.current_states.set_data(chat_id, user_id, key, value) + + def register_next_step_handler_by_chat_id( + self, chat_id: Union[int, str], callback: Callable, *args, **kwargs) -> None: + """ + Registers a callback function to be notified when new message arrives after `message`. + + Warning: In case `callback` as lambda function, saving next step handlers will not work. + + :param chat_id: The chat for which we want to handle new message. + :param callback: The callback function which next new message arrives. + :param args: Args to pass in callback func + :param kwargs: Args to pass in callback func + """ + self.next_step_backend.register_handler(chat_id, Handler(callback, *args, **kwargs)) + + def clear_step_handler(self, message: types.Message) -> None: + """ + Clears all callback functions registered by register_next_step_handler(). + + :param message: The message for which we want to handle new message after that in same chat. + """ + chat_id = message.chat.id + self.clear_step_handler_by_chat_id(chat_id) + + def clear_step_handler_by_chat_id(self, chat_id: Union[int, str]) -> None: + """ + Clears all callback functions registered by register_next_step_handler(). + + :param chat_id: The chat for which we want to clear next step handlers + """ + self.next_step_backend.clear_handlers(chat_id) + + def clear_reply_handlers(self, message: types.Message) -> None: + """ + Clears all callback functions registered by register_for_reply() and register_for_reply_by_message_id(). + + :param message: The message for which we want to clear reply handlers + """ + message_id = message.message_id + self.clear_reply_handlers_by_message_id(message_id) + + def clear_reply_handlers_by_message_id(self, message_id: int) -> None: + """ + Clears all callback functions registered by register_for_reply() and register_for_reply_by_message_id(). + + :param message_id: The message id for which we want to clear reply handlers + """ + self.reply_backend.clear_handlers(message_id) + + def _notify_next_handlers(self, new_messages): + """ + Description: TBD + + :param new_messages: + :return: + """ + for i, message in enumerate(new_messages): + need_pop = False + handlers = self.next_step_backend.get_handlers(message.chat.id) + if handlers: + for handler in handlers: + need_pop = True + self._exec_task(handler["callback"], message, *handler["args"], **handler["kwargs"]) + if need_pop: + # removing message that was detected with next_step_handler + new_messages.pop(i) + + @staticmethod + def _build_handler_dict(handler, pass_bot=False, **filters): + """ + Builds a dictionary for a handler + + :param handler: + :param filters: + :return: + """ + return { + 'function': handler, + 'pass_bot': pass_bot, + 'filters': {ftype: fvalue for ftype, fvalue in filters.items() if fvalue is not None} + # Remove None values, they are skipped in _test_filter anyway + #'filters': filters + } + + def middleware_handler(self, update_types=None): + """ + Middleware handler decorator. + + This decorator can be used to decorate functions that must be handled as middlewares before entering any other + message handlers + But, be careful and check type of the update inside the handler if more than one update_type is given + + Example: + + .. code-block:: python3 + + bot = TeleBot('TOKEN') + + # Print post message text before entering to any post_channel handlers + @bot.middleware_handler(update_types=['channel_post', 'edited_channel_post']) + def print_channel_post_text(bot_instance, channel_post): + print(channel_post.text) + + # Print update id before entering to any handlers + @bot.middleware_handler() + def print_channel_post_text(bot_instance, update): + print(update.update_id) + + :param update_types: Optional list of update types that can be passed into the middleware handler. + """ + def decorator(handler): + self.add_middleware_handler(handler, update_types) + return handler + + return decorator + + def add_middleware_handler(self, handler, update_types=None): + """ + Add middleware handler + :param handler: + :param update_types: + :return: + """ + if not apihelper.ENABLE_MIDDLEWARE: + raise RuntimeError("Middleware is not enabled. Use apihelper.ENABLE_MIDDLEWARE before initialising TeleBot.") + + if update_types: + for update_type in update_types: + self.typed_middleware_handlers[update_type].append(handler) + else: + self.default_middleware_handlers.append(handler) + + # function register_middleware_handler + def register_middleware_handler(self, callback, update_types=None): + """ + Middleware handler decorator. + + This function will create a decorator that can be used to decorate functions that must be handled as middlewares before entering any other + message handlers + But, be careful and check type of the update inside the handler if more than one update_type is given + + Example: + + bot = TeleBot('TOKEN') + + bot.register_middleware_handler(print_channel_post_text, update_types=['channel_post', 'edited_channel_post']) + + :param callback: + :param update_types: Optional list of update types that can be passed into the middleware handler. + """ + self.add_middleware_handler(callback, update_types) + + @staticmethod + def check_commands_input(commands, method_name): + if not isinstance(commands, list) or not all(isinstance(item, str) for item in commands): + logger.error(f"{method_name}: Commands filter should be list of strings (commands), unknown type supplied to the 'commands' filter list. Not able to use the supplied type.") + + @staticmethod + def check_regexp_input(regexp, method_name): + if not isinstance(regexp, str): + logger.error(f"{method_name}: Regexp filter should be string. Not able to use the supplied type.") + + def message_handler(self, commands=None, regexp=None, func=None, content_types=None, chat_types=None, **kwargs): + """ + Message handler decorator. + This decorator can be used to decorate functions that must handle certain types of messages. + All message handlers are tested in the order they were added. + + Example: + + .. code-block:: python + + bot = TeleBot('TOKEN') + + # Handles all messages which text matches regexp. + @bot.message_handler(regexp='someregexp') + def command_help(message): + bot.send_message(message.chat.id, 'Did someone call for help?') + + # Handles messages in private chat + @bot.message_handler(chat_types=['private']) # You can add more chat types + def command_help(message): + bot.send_message(message.chat.id, 'Private chat detected, sir!') + + # Handle all sent documents of type 'text/plain'. + @bot.message_handler(func=lambda message: message.document.mime_type == 'text/plain', + content_types=['document']) + def command_handle_document(message): + bot.send_message(message.chat.id, 'Document received, sir!') + + # Handle all other messages. + @bot.message_handler(func=lambda message: True, content_types=['audio', 'photo', 'voice', 'video', 'document', + 'text', 'location', 'contact', 'sticker']) + def default_command(message): + bot.send_message(message.chat.id, "This is the default command handler.") + + :param commands: Optional list of strings (commands to handle). + :param regexp: Optional regular expression. + :param func: Optional lambda function. The lambda receives the message to test as the first parameter. + It must return True if the command should handle the message. + :param content_types: Supported message content types. Must be a list. Defaults to ['text']. + :param chat_types: list of chat types + """ + if content_types is None: + content_types = ["text"] + + method_name = "message_handler" + + if commands is not None: + self.check_commands_input(commands, method_name) + if isinstance(commands, str): + commands = [commands] + + if regexp is not None: + self.check_regexp_input(regexp, method_name) + + if isinstance(content_types, str): + logger.warning("message_handler: 'content_types' filter should be List of strings (content types), not string.") + content_types = [content_types] + + def decorator(handler): + handler_dict = self._build_handler_dict(handler, + chat_types=chat_types, + content_types=content_types, + commands=commands, + regexp=regexp, + func=func, + **kwargs) + self.add_message_handler(handler_dict) + return handler + + return decorator + + def add_message_handler(self, handler_dict): + """ + Adds a message handler + Note that you should use register_message_handler to add message_handler to the bot. + + :param handler_dict: + :return: + """ + self.message_handlers.append(handler_dict) + + def register_message_handler(self, callback, content_types=None, commands=None, regexp=None, func=None, chat_types=None, pass_bot=False, **kwargs): + """ + Registers message handler. + + :param callback: function to be called + :param content_types: list of content_types + :param commands: list of commands + :param regexp: + :param func: + :param chat_types: True for private chat + :param pass_bot: Pass TeleBot to handler. + :return: decorated function + """ + method_name = "register_message_handler" + + if commands is not None: + self.check_commands_input(commands, method_name) + if isinstance(commands, str): + commands = [commands] + + if regexp is not None: + self.check_regexp_input(regexp, method_name) + + if isinstance(content_types, str): + logger.warning("register_message_handler: 'content_types' filter should be List of strings (content types), not string.") + content_types = [content_types] + + + + handler_dict = self._build_handler_dict(callback, + chat_types=chat_types, + content_types=content_types, + commands=commands, + regexp=regexp, + func=func, + pass_bot=pass_bot, + **kwargs) + self.add_message_handler(handler_dict) + + def edited_message_handler(self, commands=None, regexp=None, func=None, content_types=None, chat_types=None, **kwargs): + """ + Edit message handler decorator + + :param commands: + :param regexp: + :param func: + :param content_types: + :param chat_types: list of chat types + :param kwargs: + :return: + """ + if content_types is None: + content_types = ["text"] + + method_name = "edited_message_handler" + + if commands is not None: + self.check_commands_input(commands, method_name) + if isinstance(commands, str): + commands = [commands] + + if regexp is not None: + self.check_regexp_input(regexp, method_name) + + if isinstance(content_types, str): + logger.warning("edited_message_handler: 'content_types' filter should be List of strings (content types), not string.") + content_types = [content_types] + + def decorator(handler): + handler_dict = self._build_handler_dict(handler, + chat_types=chat_types, + content_types=content_types, + commands=commands, + regexp=regexp, + func=func, + **kwargs) + self.add_edited_message_handler(handler_dict) + return handler + + return decorator + + def add_edited_message_handler(self, handler_dict): + """ + Adds the edit message handler + Note that you should use register_edited_message_handler to add edited_message_handler to the bot. + :param handler_dict: + :return: + """ + self.edited_message_handlers.append(handler_dict) + + def register_edited_message_handler(self, callback, content_types=None, commands=None, regexp=None, func=None, chat_types=None, pass_bot=False, **kwargs): + """ + Registers edited message handler. + + :param callback: function to be called + :param content_types: list of content_types + :param commands: list of commands + :param regexp: + :param func: + :param chat_types: True for private chat + :param pass_bot: Pass TeleBot to handler. + :return: decorated function + """ + method_name = "register_edited_message_handler" + + if commands is not None: + self.check_commands_input(commands, method_name) + if isinstance(commands, str): + commands = [commands] + + if regexp is not None: + self.check_regexp_input(regexp, method_name) + + if isinstance(content_types, str): + logger.warning("register_edited_message_handler: 'content_types' filter should be List of strings (content types), not string.") + content_types = [content_types] + + handler_dict = self._build_handler_dict(callback, + chat_types=chat_types, + content_types=content_types, + commands=commands, + regexp=regexp, + func=func, + pass_bot=pass_bot, + **kwargs) + self.add_edited_message_handler(handler_dict) + + + def channel_post_handler(self, commands=None, regexp=None, func=None, content_types=None, **kwargs): + """ + Channel post handler decorator + + :param commands: + :param regexp: + :param func: + :param content_types: + :param kwargs: + :return: + """ + if content_types is None: + content_types = ["text"] + + method_name = "channel_post_handler" + + if commands is not None: + self.check_commands_input(commands, method_name) + if isinstance(commands, str): + commands = [commands] + + if regexp is not None: + self.check_regexp_input(regexp, method_name) + + if isinstance(content_types, str): + logger.warning("channel_post_handler: 'content_types' filter should be List of strings (content types), not string.") + content_types = [content_types] + + def decorator(handler): + handler_dict = self._build_handler_dict(handler, + content_types=content_types, + commands=commands, + regexp=regexp, + func=func, + **kwargs) + self.add_channel_post_handler(handler_dict) + return handler + + return decorator + + def add_channel_post_handler(self, handler_dict): + """ + Adds channel post handler + Note that you should use register_channel_post_handler to add channel_post_handler to the bot. + + :param handler_dict: + :return: + """ + self.channel_post_handlers.append(handler_dict) + + def register_channel_post_handler(self, callback, content_types=None, commands=None, regexp=None, func=None, pass_bot=False, **kwargs): + """ + Registers channel post message handler. + + :param callback: function to be called + :param content_types: list of content_types + :param commands: list of commands + :param regexp: + :param func: + :param pass_bot: Pass TeleBot to handler. + :return: decorated function + """ + method_name = "register_channel_post_handler" + + if commands is not None: + self.check_commands_input(commands, method_name) + if isinstance(commands, str): + commands = [commands] + + if regexp is not None: + self.check_regexp_input(regexp, method_name) + + if isinstance(content_types, str): + logger.warning("register_channel_post_handler: 'content_types' filter should be List of strings (content types), not string.") + content_types = [content_types] + + handler_dict = self._build_handler_dict(callback, + content_types=content_types, + commands=commands, + regexp=regexp, + func=func, + pass_bot=pass_bot, + **kwargs) + self.add_channel_post_handler(handler_dict) + + def edited_channel_post_handler(self, commands=None, regexp=None, func=None, content_types=None, **kwargs): + """ + Edit channel post handler decorator + + :param commands: + :param regexp: + :param func: + :param content_types: + :param kwargs: + :return: + """ + if content_types is None: + content_types = ["text"] + + method_name = "edited_channel_post_handler" + + if commands is not None: + self.check_commands_input(commands, method_name) + if isinstance(commands, str): + commands = [commands] + + if regexp is not None: + self.check_regexp_input(regexp, method_name) + + if isinstance(content_types, str): + logger.warning("edited_channel_post_handler: 'content_types' filter should be List of strings (content types), not string.") + content_types = [content_types] + + def decorator(handler): + handler_dict = self._build_handler_dict(handler, + content_types=content_types, + commands=commands, + regexp=regexp, + func=func, + **kwargs) + self.add_edited_channel_post_handler(handler_dict) + return handler + + return decorator + + def add_edited_channel_post_handler(self, handler_dict): + """ + Adds the edit channel post handler + Note that you should use register_edited_channel_post_handler to add edited_channel_post_handler to the bot. + + :param handler_dict: + :return: + """ + self.edited_channel_post_handlers.append(handler_dict) + + def register_edited_channel_post_handler(self, callback, content_types=None, commands=None, regexp=None, func=None, pass_bot=False, **kwargs): + """ + Registers edited channel post message handler. + + :param callback: function to be called + :param content_types: list of content_types + :param commands: list of commands + :param regexp: + :param func: + :param pass_bot: Pass TeleBot to handler. + :return: decorated function + """ + method_name = "register_edited_channel_post_handler" + + if commands is not None: + self.check_commands_input(commands, method_name) + if isinstance(commands, str): + commands = [commands] + + if regexp is not None: + self.check_regexp_input(regexp, method_name) + + if isinstance(content_types, str): + logger.warning("register_edited_channel_post_handler: 'content_types' filter should be List of strings (content types), not string.") + content_types = [content_types] + + handler_dict = self._build_handler_dict(callback, + content_types=content_types, + commands=commands, + regexp=regexp, + func=func, + pass_bot=pass_bot, + **kwargs) + self.add_edited_channel_post_handler(handler_dict) + + def inline_handler(self, func, **kwargs): + """ + Inline call handler decorator + + :param func: + :param kwargs: + :return: + """ + def decorator(handler): + handler_dict = self._build_handler_dict(handler, func=func, **kwargs) + self.add_inline_handler(handler_dict) + return handler + + return decorator + + def add_inline_handler(self, handler_dict): + """ + Adds inline call handler + Note that you should use register_inline_handler to add inline_handler to the bot. + + :param handler_dict: + :return: + """ + self.inline_handlers.append(handler_dict) + + def register_inline_handler(self, callback, func, pass_bot=False, **kwargs): + """ + Registers inline handler. + + :param callback: function to be called + :param func: + :param pass_bot: Pass TeleBot to handler. + :return: decorated function + """ + handler_dict = self._build_handler_dict(callback, func=func, pass_bot=pass_bot, **kwargs) + self.add_inline_handler(handler_dict) + + def chosen_inline_handler(self, func, **kwargs): + """ + Description: TBD + + :param func: + :param kwargs: + :return: + """ + def decorator(handler): + handler_dict = self._build_handler_dict(handler, func=func, **kwargs) + self.add_chosen_inline_handler(handler_dict) + return handler + + return decorator + + def add_chosen_inline_handler(self, handler_dict): + """ + Description: TBD + Note that you should use register_chosen_inline_handler to add chosen_inline_handler to the bot. + + :param handler_dict: + :return: + """ + self.chosen_inline_handlers.append(handler_dict) + + def register_chosen_inline_handler(self, callback, func, pass_bot=False, **kwargs): + """ + Registers chosen inline handler. + :param callback: function to be called + :param func: + :param pass_bot: Pass TeleBot to handler. + :return: decorated function + """ + handler_dict = self._build_handler_dict(callback, func=func, pass_bot=pass_bot, **kwargs) + self.add_chosen_inline_handler(handler_dict) + + def callback_query_handler(self, func, **kwargs): + """ + Callback request handler decorator + + :param func: + :param kwargs: + :return: + """ + def decorator(handler): + handler_dict = self._build_handler_dict(handler, func=func, **kwargs) + self.add_callback_query_handler(handler_dict) + return handler + + return decorator + + def add_callback_query_handler(self, handler_dict): + """ + Adds a callback request handler + Note that you should use register_callback_query_handler to add callback_query_handler to the bot. + + :param handler_dict: + :return: + """ + self.callback_query_handlers.append(handler_dict) + + def register_callback_query_handler(self, callback, func, pass_bot=False, **kwargs): + """ + Registers callback query handler. + + :param callback: function to be called + :param func: + :param pass_bot: Pass TeleBot to handler. + :return: decorated function + """ + handler_dict = self._build_handler_dict(callback, func=func, pass_bot=pass_bot, **kwargs) + self.add_callback_query_handler(handler_dict) + + def shipping_query_handler(self, func, **kwargs): + """ + Shipping request handler + + :param func: + :param kwargs: + :return: + """ + def decorator(handler): + handler_dict = self._build_handler_dict(handler, func=func, **kwargs) + self.add_shipping_query_handler(handler_dict) + return handler + + return decorator + + def add_shipping_query_handler(self, handler_dict): + """ + Adds a shipping request handler. + Note that you should use register_shipping_query_handler to add shipping_query_handler to the bot. + + :param handler_dict: + :return: + """ + self.shipping_query_handlers.append(handler_dict) + + def register_shipping_query_handler(self, callback, func, pass_bot=False, **kwargs): + """ + Registers shipping query handler. + + :param callback: function to be called + :param func: + :param pass_bot: Pass TeleBot to handler. + :return: decorated function + """ + handler_dict = self._build_handler_dict(callback, func=func, pass_bot=pass_bot, **kwargs) + self.add_shipping_query_handler(handler_dict) + + def pre_checkout_query_handler(self, func, **kwargs): + """ + Pre-checkout request handler + + :param func: + :param kwargs: + :return: + """ + def decorator(handler): + handler_dict = self._build_handler_dict(handler, func=func, **kwargs) + self.add_pre_checkout_query_handler(handler_dict) + return handler + + return decorator + + def add_pre_checkout_query_handler(self, handler_dict): + """ + Adds a pre-checkout request handler + Note that you should use register_pre_checkout_query_handler to add pre_checkout_query_handler to the bot. + + :param handler_dict: + :return: + """ + self.pre_checkout_query_handlers.append(handler_dict) + + def register_pre_checkout_query_handler(self, callback, func, pass_bot=False, **kwargs): + """ + Registers pre-checkout request handler. + + :param callback: function to be called + :param func: + :param pass_bot: Pass TeleBot to handler. + :return: decorated function + """ + handler_dict = self._build_handler_dict(callback, func=func, pass_bot=pass_bot, **kwargs) + self.add_pre_checkout_query_handler(handler_dict) + + def poll_handler(self, func, **kwargs): + """ + Poll request handler + + :param func: + :param kwargs: + :return: + """ + def decorator(handler): + handler_dict = self._build_handler_dict(handler, func=func, **kwargs) + self.add_poll_handler(handler_dict) + return handler + + return decorator + + def add_poll_handler(self, handler_dict): + """ + Adds a poll request handler + Note that you should use register_poll_handler to add poll_handler to the bot. + + :param handler_dict: + :return: + """ + self.poll_handlers.append(handler_dict) + + def register_poll_handler(self, callback, func, pass_bot=False, **kwargs): + """ + Registers poll handler. + + :param callback: function to be called + :param func: + :param pass_bot: Pass TeleBot to handler. + :return: decorated function + """ + handler_dict = self._build_handler_dict(callback, func=func, pass_bot=pass_bot, **kwargs) + self.add_poll_handler(handler_dict) + + def poll_answer_handler(self, func=None, **kwargs): + """ + Poll_answer request handler + + :param func: + :param kwargs: + :return: + """ + def decorator(handler): + handler_dict = self._build_handler_dict(handler, func=func, **kwargs) + self.add_poll_answer_handler(handler_dict) + return handler + + return decorator + + def add_poll_answer_handler(self, handler_dict): + """ + Adds a poll_answer request handler. + Note that you should use register_poll_answer_handler to add poll_answer_handler to the bot. + + :param handler_dict: + :return: + """ + self.poll_answer_handlers.append(handler_dict) + + def register_poll_answer_handler(self, callback, func, pass_bot=False, **kwargs): + """ + Registers poll answer handler. + + :param callback: function to be called + :param func: + :param pass_bot: Pass TeleBot to handler. + :return: decorated function + """ + handler_dict = self._build_handler_dict(callback, func=func, pass_bot=pass_bot, **kwargs) + self.add_poll_answer_handler(handler_dict) + + def my_chat_member_handler(self, func=None, **kwargs): + """ + my_chat_member handler. + + :param func: + :param kwargs: + :return: + """ + def decorator(handler): + handler_dict = self._build_handler_dict(handler, func=func, **kwargs) + self.add_my_chat_member_handler(handler_dict) + return handler + + return decorator + + def add_my_chat_member_handler(self, handler_dict): + """ + Adds a my_chat_member handler. + Note that you should use register_my_chat_member_handler to add my_chat_member_handler to the bot. + + :param handler_dict: + :return: + """ + self.my_chat_member_handlers.append(handler_dict) + + def register_my_chat_member_handler(self, callback, func=None, pass_bot=False, **kwargs): + """ + Registers my chat member handler. + + :param callback: function to be called + :param func: + :param pass_bot: Pass TeleBot to handler. + :return: decorated function + """ + handler_dict = self._build_handler_dict(callback, func=func, pass_bot=pass_bot, **kwargs) + self.add_my_chat_member_handler(handler_dict) + + def chat_member_handler(self, func=None, **kwargs): + """ + chat_member handler. + + :param func: + :param kwargs: + :return: + """ + def decorator(handler): + handler_dict = self._build_handler_dict(handler, func=func, **kwargs) + self.add_chat_member_handler(handler_dict) + return handler + + return decorator + + def add_chat_member_handler(self, handler_dict): + """ + Adds a chat_member handler. + Note that you should use register_chat_member_handler to add chat_member_handler to the bot. + + :param handler_dict: + :return: + """ + self.chat_member_handlers.append(handler_dict) + + def register_chat_member_handler(self, callback, func=None, pass_bot=False, **kwargs): + """ + Registers chat member handler. + + :param callback: function to be called + :param func: + :param pass_bot: Pass TeleBot to handler. + :return: decorated function + """ + handler_dict = self._build_handler_dict(callback, func=func, pass_bot=pass_bot, **kwargs) + self.add_chat_member_handler(handler_dict) + + def chat_join_request_handler(self, func=None, **kwargs): + """ + chat_join_request handler + + :param func: + :param kwargs: + :return: + """ + def decorator(handler): + handler_dict = self._build_handler_dict(handler, func=func, **kwargs) + self.add_chat_join_request_handler(handler_dict) + return handler + + return decorator + + def add_chat_join_request_handler(self, handler_dict): + """ + Adds a chat_join_request handler. + Note that you should use register_chat_join_request_handler to add chat_join_request_handler to the bot. + + :param handler_dict: + :return: + """ + self.chat_join_request_handlers.append(handler_dict) + + def register_chat_join_request_handler(self, callback, func=None, pass_bot=False, **kwargs): + """ + Registers chat join request handler. + :param callback: function to be called + :param func: + :param pass_bot: Pass TeleBot to handler. + :return: decorated function + """ + handler_dict = self._build_handler_dict(callback, func=func, pass_bot=pass_bot, **kwargs) + self.add_chat_join_request_handler(handler_dict) + + def _test_message_handler(self, message_handler, message): + """ + Test message handler + + :param message_handler: + :param message: + :return: + """ + for message_filter, filter_value in message_handler['filters'].items(): + if filter_value is None: + continue + + if not self._test_filter(message_filter, filter_value, message): + return False + + return True + + def add_custom_filter(self, custom_filter: Union[SimpleCustomFilter, AdvancedCustomFilter]): + """ + Create custom filter. + + custom_filter: Class with check(message) method. + :param custom_filter: Custom filter class with key. + """ + self.custom_filters[custom_filter.key] = custom_filter + + def _test_filter(self, message_filter, filter_value, message): + """ + Test filters + + :param message_filter: Filter type passed in handler + :param filter_value: Filter value passed in handler + :param message: Message to test + :return: True if filter conforms + """ + if message_filter == 'content_types': + return message.content_type in filter_value + elif message_filter == 'regexp': + return message.content_type == 'text' and re.search(filter_value, message.text, re.IGNORECASE) + elif message_filter == 'commands': + return message.content_type == 'text' and util.extract_command(message.text) in filter_value + elif message_filter == 'chat_types': + return message.chat.type in filter_value + elif message_filter == 'func': + return filter_value(message) + elif self.custom_filters and message_filter in self.custom_filters: + return self._check_filter(message_filter,filter_value,message) + else: + return False + + def _check_filter(self, message_filter, filter_value, message): + filter_check = self.custom_filters.get(message_filter) + if not filter_check: + return False + elif isinstance(filter_check, SimpleCustomFilter): + return filter_value == filter_check.check(message) + elif isinstance(filter_check, AdvancedCustomFilter): + return filter_check.check(message, filter_value) + else: + logger.error("Custom filter: wrong type. Should be SimpleCustomFilter or AdvancedCustomFilter.") + return False + + # middleware check-up method + def _check_middleware(self, update_type): + """ + Check middleware + + :param message: + :return: + """ + middlewares = None + if self.middlewares: + middlewares = [i for i in self.middlewares if update_type in i.update_types] + return middlewares + + def _run_middlewares_and_handler(self, message, handlers, middlewares, *args, **kwargs): + """ + This class is made to run handler and middleware in queue. + + :param handler: handler that should be executed. + :param middleware: middleware that should be executed. + :return: + """ + data = {} + params =[] + handler_error = None + skip_handler = False + if middlewares: + for middleware in middlewares: + result = middleware.pre_process(message, data) + # We will break this loop if CancelUpdate is returned + # Also, we will not run other middlewares + if isinstance(result, CancelUpdate): + return + elif isinstance(result, SkipHandler) and skip_handler is False: + skip_handler = True + + try: + if handlers and not skip_handler: + for handler in handlers: + process_handler = self._test_message_handler(handler, message) + if not process_handler: continue + else: + for i in inspect.signature(handler['function']).parameters: + params.append(i) + if len(params) == 1: + handler['function'](message) + + elif len(params) == 2: + if handler.get('pass_bot') is True: + handler['function'](message, self) + + elif handler.get('pass_bot') is False: + handler['function'](message, data) + + elif len(params) == 3: + if params[2] == 'bot' and handler.get('pass_bot') is True: + handler['function'](message, data, self) + + else: + handler['function'](message, self, data) + + except Exception as e: + handler_error = e + + if not middlewares: + if self.exception_handler: + return self.exception_handler.handle(e) + logging.error(str(e)) + return + if middlewares: + for middleware in middlewares: + middleware.post_process(message, data, handler_error) + + + + + def _notify_command_handlers(self, handlers, new_messages, update_type): + """ + Notifies command handlers. + + :param handlers: + :param new_messages: + :return: + """ + if len(handlers) == 0 and not self.use_class_middlewares: + return + + for message in new_messages: + if self.use_class_middlewares: + middleware = self._check_middleware(update_type) + self._exec_task(self._run_middlewares_and_handler, message, handlers=handlers, middlewares=middleware) + return + else: + for message_handler in handlers: + if self._test_message_handler(message_handler, message): + self._exec_task(message_handler['function'], message, pass_bot=message_handler['pass_bot'], task_type='handler') + break diff --git a/telebot/__pycache__/__init__.cpython-39.pyc b/telebot/__pycache__/__init__.cpython-39.pyc new file mode 100644 index 0000000..fa37469 Binary files /dev/null and b/telebot/__pycache__/__init__.cpython-39.pyc differ diff --git a/telebot/__pycache__/apihelper.cpython-39.pyc b/telebot/__pycache__/apihelper.cpython-39.pyc new file mode 100644 index 0000000..31dc600 Binary files /dev/null and b/telebot/__pycache__/apihelper.cpython-39.pyc differ diff --git a/telebot/__pycache__/custom_filters.cpython-39.pyc b/telebot/__pycache__/custom_filters.cpython-39.pyc new file mode 100644 index 0000000..2dbadb1 Binary files /dev/null and b/telebot/__pycache__/custom_filters.cpython-39.pyc differ diff --git a/telebot/__pycache__/handler_backends.cpython-39.pyc b/telebot/__pycache__/handler_backends.cpython-39.pyc new file mode 100644 index 0000000..d5f1b7d Binary files /dev/null and b/telebot/__pycache__/handler_backends.cpython-39.pyc differ diff --git a/telebot/__pycache__/types.cpython-39.pyc b/telebot/__pycache__/types.cpython-39.pyc new file mode 100644 index 0000000..4f3ed0a Binary files /dev/null and b/telebot/__pycache__/types.cpython-39.pyc differ diff --git a/telebot/__pycache__/util.cpython-39.pyc b/telebot/__pycache__/util.cpython-39.pyc new file mode 100644 index 0000000..6fb78f8 Binary files /dev/null and b/telebot/__pycache__/util.cpython-39.pyc differ diff --git a/telebot/apihelper.py b/telebot/apihelper.py new file mode 100644 index 0000000..67a8e7c --- /dev/null +++ b/telebot/apihelper.py @@ -0,0 +1,1776 @@ +# -*- coding: utf-8 -*- +import time +from datetime import datetime + +try: + import ujson as json +except ImportError: + import json + +import requests +from requests.exceptions import HTTPError, ConnectionError, Timeout +from requests.adapters import HTTPAdapter + +try: + # noinspection PyUnresolvedReferences + from requests.packages.urllib3 import fields + format_header_param = fields.format_header_param +except ImportError: + format_header_param = None +import telebot +from telebot import types +from telebot import util + +logger = telebot.logger + +proxy = None +session = None + +API_URL = None +FILE_URL = None + +CONNECT_TIMEOUT = 15 +READ_TIMEOUT = 30 + +LONG_POLLING_TIMEOUT = 10 # Should be positive, short polling should be used for testing purposes only (https://core.telegram.org/bots/api#getupdates) + +SESSION_TIME_TO_LIVE = 600 # In seconds. None - live forever, 0 - one-time + +RETRY_ON_ERROR = False +RETRY_TIMEOUT = 2 +MAX_RETRIES = 15 +RETRY_ENGINE = 1 + +CUSTOM_SERIALIZER = None +CUSTOM_REQUEST_SENDER = None + +ENABLE_MIDDLEWARE = False + + + +def _get_req_session(reset=False): + if SESSION_TIME_TO_LIVE: + # If session TTL is set - check time passed + creation_date = util.per_thread('req_session_time', lambda: datetime.now(), reset) + # noinspection PyTypeChecker + if (datetime.now() - creation_date).total_seconds() > SESSION_TIME_TO_LIVE: + # Force session reset + reset = True + # Save reset time + util.per_thread('req_session_time', lambda: datetime.now(), True) + + if SESSION_TIME_TO_LIVE == 0: + # Session is one-time use + return requests.sessions.Session() + else: + # Session lives some time or forever once created. Default + return util.per_thread('req_session', lambda: session if session else requests.sessions.Session(), reset) + + +def _make_request(token, method_name, method='get', params=None, files=None): + """ + Makes a request to the Telegram API. + :param token: The bot's API token. (Created with @BotFather) + :param method_name: Name of the API method to be called. (E.g. 'getUpdates') + :param method: HTTP method to be used. Defaults to 'get'. + :param params: Optional parameters. Should be a dictionary with key-value pairs. + :param files: Optional files. + :return: The result parsed to a JSON dictionary. + """ + if not token: + raise Exception('Bot token is not defined') + if API_URL: + # noinspection PyUnresolvedReferences + request_url = API_URL.format(token, method_name) + else: + request_url = "https://api.telegram.org/bot{0}/{1}".format(token, method_name) + + logger.debug("Request: method={0} url={1} params={2} files={3}".format(method, request_url, params, files).replace(token, token.split(':')[0] + ":{TOKEN}")) + read_timeout = READ_TIMEOUT + connect_timeout = CONNECT_TIMEOUT + if files and format_header_param: + fields.format_header_param = _no_encode(format_header_param) + if params: + if 'timeout' in params: + read_timeout = params.pop('timeout') + connect_timeout = read_timeout +# if 'connect-timeout' in params: +# connect_timeout = params.pop('connect-timeout') + 10 + if 'long_polling_timeout' in params: + # For getUpdates: it's the only function with timeout parameter on the BOT API side + long_polling_timeout = params.pop('long_polling_timeout') + params['timeout'] = long_polling_timeout + # Long polling hangs for a given time. Read timeout should be greater that long_polling_timeout + read_timeout = max(long_polling_timeout + 5, read_timeout) + # Lets stop suppose that user is stupid and assume that he knows what he do... + # read_timeout = read_timeout + 10 + # connect_timeout = connect_timeout + 10 + + params = params or None # Set params to None if empty + + result = None + if RETRY_ON_ERROR and RETRY_ENGINE == 1: + got_result = False + current_try = 0 + while not got_result and current_try 0: + ret = ret[:-1] + return '[' + ret + ']' + + +def _convert_markup(markup): + if isinstance(markup, types.JsonSerializable): + return markup.to_json() + return markup + + +def _convert_entites(entites): + if entites is None: + return None + elif len(entites) == 0: + return [] + elif isinstance(entites[0], types.JsonSerializable): + return [entity.to_json() for entity in entites] + else: + return entites + + +def _convert_poll_options(poll_options): + if poll_options is None: + return None + elif len(poll_options) == 0: + return [] + elif isinstance(poll_options[0], str): + # Compatibility mode with previous bug when only list of string was accepted as poll_options + return poll_options + elif isinstance(poll_options[0], types.PollOption): + return [option.text for option in poll_options] + else: + return poll_options + + +def convert_input_media(media): + if isinstance(media, types.InputMedia): + return media.convert_input_media() + return None, None + + +def convert_input_media_array(array): + media = [] + files = {} + for input_media in array: + if isinstance(input_media, types.InputMedia): + media_dict = input_media.to_dict() + if media_dict['media'].startswith('attach://'): + key = media_dict['media'].replace('attach://', '') + files[key] = input_media.media + media.append(media_dict) + return json.dumps(media), files + + +def _no_encode(func): + def wrapper(key, val): + if key == 'filename': + return u'{0}={1}'.format(key, val) + else: + return func(key, val) + + return wrapper + + +class ApiException(Exception): + """ + This class represents a base Exception thrown when a call to the Telegram API fails. + In addition to an informative message, it has a `function_name` and a `result` attribute, which respectively + contain the name of the failed function and the returned result that made the function to be considered as + failed. + """ + + def __init__(self, msg, function_name, result): + super(ApiException, self).__init__("A request to the Telegram API was unsuccessful. {0}".format(msg)) + self.function_name = function_name + self.result = result + +class ApiHTTPException(ApiException): + """ + This class represents an Exception thrown when a call to the + Telegram API server returns HTTP code that is not 200. + """ + def __init__(self, function_name, result): + super(ApiHTTPException, self).__init__( + "The server returned HTTP {0} {1}. Response body:\n[{2}]" \ + .format(result.status_code, result.reason, result.text.encode('utf8')), + function_name, + result) + +class ApiInvalidJSONException(ApiException): + """ + This class represents an Exception thrown when a call to the + Telegram API server returns invalid json. + """ + def __init__(self, function_name, result): + super(ApiInvalidJSONException, self).__init__( + "The server returned an invalid JSON response. Response body:\n[{0}]" \ + .format(result.text.encode('utf8')), + function_name, + result) + +class ApiTelegramException(ApiException): + """ + This class represents an Exception thrown when a Telegram API returns error code. + """ + def __init__(self, function_name, result, result_json): + super(ApiTelegramException, self).__init__( + "Error code: {0}. Description: {1}" \ + .format(result_json['error_code'], result_json['description']), + function_name, + result) + self.result_json = result_json + self.error_code = result_json['error_code'] + self.description = result_json['description'] + diff --git a/telebot/async_telebot.py b/telebot/async_telebot.py new file mode 100644 index 0000000..7b45ed9 --- /dev/null +++ b/telebot/async_telebot.py @@ -0,0 +1,3357 @@ +# -*- coding: utf-8 -*- +from datetime import datetime + +import logging +import re +import time +import traceback +from typing import Any, List, Optional, Union + +# this imports are used to avoid circular import error +import telebot.util +import telebot.types + + +# storages +from telebot.asyncio_storage import StateMemoryStorage, StatePickleStorage +from telebot.asyncio_handler_backends import CancelUpdate, SkipHandler + +from inspect import signature + +from telebot import logger + +from telebot import util, types, asyncio_helper +import asyncio +from telebot import asyncio_filters + + +REPLY_MARKUP_TYPES = Union[ + types.InlineKeyboardMarkup, types.ReplyKeyboardMarkup, + types.ReplyKeyboardRemove, types.ForceReply] + + +""" +Module : telebot +""" + + +class Handler: + """ + Class for (next step|reply) handlers + """ + def __init__(self, callback, *args, **kwargs): + self.callback = callback + self.args = args + self.kwargs = kwargs + + def __getitem__(self, item): + return getattr(self, item) + + +class ExceptionHandler: + """ + Class for handling exceptions while Polling + """ + + # noinspection PyMethodMayBeStatic,PyUnusedLocal + def handle(self, exception): + return False + + +class AsyncTeleBot: + """ + This is the main asynchronous class for Bot. + + It allows you to add handlers for different kind of updates. + + Usage: + + .. code-block:: python + + from telebot.async_telebot import AsyncTeleBot + bot = AsyncTeleBot('token') # get token from @BotFather + + See more examples in examples/ directory: + https://github.com/eternnoir/pyTelegramBotAPI/tree/master/examples + + """ + + def __init__(self, token: str, parse_mode: Optional[str]=None, offset=None, + exception_handler=None, state_storage=StateMemoryStorage()) -> None: # TODO: ADD TYPEHINTS + self.token = token + + self.offset = offset + self.token = token + self.parse_mode = parse_mode + self.update_listener = [] + + + + self.exception_handler = exception_handler + + self.message_handlers = [] + self.edited_message_handlers = [] + self.channel_post_handlers = [] + self.edited_channel_post_handlers = [] + self.inline_handlers = [] + self.chosen_inline_handlers = [] + self.callback_query_handlers = [] + self.shipping_query_handlers = [] + self.pre_checkout_query_handlers = [] + self.poll_handlers = [] + self.poll_answer_handlers = [] + self.my_chat_member_handlers = [] + self.chat_member_handlers = [] + self.chat_join_request_handlers = [] + self.custom_filters = {} + self.state_handlers = [] + + self.current_states = state_storage + + + self.middlewares = [] + + async def close_session(self): + """ + Closes existing session of aiohttp. + Use this function if you stop polling. + """ + await asyncio_helper.session_manager.session.close() + async def get_updates(self, offset: Optional[int]=None, limit: Optional[int]=None, + timeout: Optional[int]=None, allowed_updates: Optional[List]=None, request_timeout: Optional[int]=None) -> List[types.Update]: + json_updates = await asyncio_helper.get_updates(self.token, offset, limit, timeout, allowed_updates, request_timeout) + return [types.Update.de_json(ju) for ju in json_updates] + + async def polling(self, non_stop: bool=False, skip_pending=False, interval: int=0, timeout: int=20, + request_timeout: int=20, allowed_updates: Optional[List[str]]=None, + none_stop: Optional[bool]=None): + """ + This allows the bot to retrieve Updates automatically and notify listeners and message handlers accordingly. + + Warning: Do not call this function more than once! + + Always get updates. + + :param interval: Delay between two update retrivals + :param non_stop: Do not stop polling when an ApiException occurs. + :param timeout: Request connection timeout + :param skip_pending: skip old updates + :param request_timeout: Timeout in seconds for a request. + :param allowed_updates: A list of the update types you want your bot to receive. + For example, specify [“message”, “edited_channel_post”, “callback_query”] to only receive updates of these types. + See util.update_types for a complete list of available update types. + Specify an empty list to receive all update types except chat_member (default). + If not specified, the previous setting will be used. + + Please note that this parameter doesn't affect updates created before the call to the get_updates, + so unwanted updates may be received for a short period of time. + :param none_stop: Deprecated, use non_stop. Old typo f***up compatibility + :return: + """ + if none_stop is not None: + non_stop = none_stop + + if skip_pending: + await self.skip_updates() + await self._process_polling(non_stop, interval, timeout, request_timeout, allowed_updates) + + async def infinity_polling(self, timeout: int=20, skip_pending: bool=False, request_timeout: int=20, logger_level=logging.ERROR, + allowed_updates: Optional[List[str]]=None, *args, **kwargs): + """ + Wrap polling with infinite loop and exception handling to avoid bot stops polling. + + :param timeout: Request connection timeout + :param request_timeout: Timeout in seconds for long polling (see API docs) + :param skip_pending: skip old updates + :param logger_level: Custom logging level for infinity_polling logging. + Use logger levels from logging as a value. None/NOTSET = no error logging + :param allowed_updates: A list of the update types you want your bot to receive. + For example, specify [“message”, “edited_channel_post”, “callback_query”] to only receive updates of these types. + See util.update_types for a complete list of available update types. + Specify an empty list to receive all update types except chat_member (default). + If not specified, the previous setting will be used. + + Please note that this parameter doesn't affect updates created before the call to the get_updates, + so unwanted updates may be received for a short period of time. + """ + if skip_pending: + await self.skip_updates() + self._polling = True + while self._polling: + try: + await self._process_polling(non_stop=True, timeout=timeout, request_timeout=request_timeout, + allowed_updates=allowed_updates, *args, **kwargs) + except Exception as e: + if logger_level and logger_level >= logging.ERROR: + logger.error("Infinity polling exception: %s", str(e)) + if logger_level and logger_level >= logging.DEBUG: + logger.error("Exception traceback:\n%s", traceback.format_exc()) + time.sleep(3) + continue + if logger_level and logger_level >= logging.INFO: + logger.error("Infinity polling: polling exited") + if logger_level and logger_level >= logging.INFO: + logger.error("Break infinity polling") + + async def _process_polling(self, non_stop: bool=False, interval: int=0, timeout: int=20, + request_timeout: int=20, allowed_updates: Optional[List[str]]=None): + """ + Function to process polling. + + :param non_stop: Do not stop polling when an ApiException occurs. + :param interval: Delay between two update retrivals + :param timeout: Request connection timeout + :param request_timeout: Timeout in seconds for long polling (see API docs) + :param allowed_updates: A list of the update types you want your bot to receive. + For example, specify [“message”, “edited_channel_post”, “callback_query”] to only receive updates of these types. + See util.update_types for a complete list of available update types. + Specify an empty list to receive all update types except chat_member (default). + If not specified, the previous setting will be used. + + Please note that this parameter doesn't affect updates created before the call to the get_updates, + so unwanted updates may be received for a short period of time. + :return: + + """ + self._polling = True + + try: + while self._polling: + try: + + updates = await self.get_updates(offset=self.offset, allowed_updates=allowed_updates, timeout=timeout, request_timeout=request_timeout) + if updates: + self.offset = updates[-1].update_id + 1 + asyncio.create_task(self.process_new_updates(updates)) # Seperate task for processing updates + if interval: await asyncio.sleep(interval) + + except KeyboardInterrupt: + return + except asyncio.CancelledError: + return + except asyncio_helper.RequestTimeout as e: + logger.error(str(e)) + if non_stop: + await asyncio.sleep(2) + continue + else: + return + except asyncio_helper.ApiTelegramException as e: + logger.error(str(e)) + if non_stop: + continue + else: + break + except Exception as e: + logger.error('Cause exception while getting updates.') + if non_stop: + logger.error(str(e)) + await asyncio.sleep(3) + continue + else: + raise e + finally: + self._polling = False + await self.close_session() + logger.warning('Polling is stopped.') + + def _loop_create_task(self, coro): + return asyncio.create_task(coro) + + async def _process_updates(self, handlers, messages, update_type): + """ + Process updates. + + :param handlers: + :param messages: + :return: + """ + tasks = [] + for message in messages: + middleware = await self.process_middlewares(update_type) + tasks.append(self._run_middlewares_and_handlers(handlers, message, middleware)) + await asyncio.gather(*tasks) + + async def _run_middlewares_and_handlers(self, handlers, message, middlewares): + handler_error = None + data = {} + process_handler = True + + if middlewares: + for middleware in middlewares: + middleware_result = await middleware.pre_process(message, data) + if isinstance(middleware_result, SkipHandler): + await middleware.post_process(message, data, handler_error) + process_handler = False + if isinstance(middleware_result, CancelUpdate): + return + for handler in handlers: + if not process_handler: + break + + process_update = await self._test_message_handler(handler, message) + if not process_update: + continue + elif process_update: + try: + params = [] + + for i in signature(handler['function']).parameters: + params.append(i) + if len(params) == 1: + await handler['function'](message) + break + elif len(params) == 2: + if handler['pass_bot']: + await handler['function'](message, self) + break + else: + await handler['function'](message, data) + break + elif len(params) == 3: + if handler['pass_bot'] and params[1] == 'bot': + await handler['function'](message, self, data) + break + else: + await handler['function'](message, data) + break + except Exception as e: + handler_error = e + + if not middlewares: + if self.exception_handler: + return self.exception_handler.handle(e) + logging.error(str(e)) + return + + if middlewares: + for middleware in middlewares: + await middleware.post_process(message, data, handler_error) + # update handling + async def process_new_updates(self, updates): + """ + Process new updates. + Just pass list of updates - each update should be + instance of Update object. + + :param updates: list of updates + """ + upd_count = len(updates) + logger.info('Received {0} new updates'.format(upd_count)) + if upd_count == 0: return + + new_messages = None + new_edited_messages = None + new_channel_posts = None + new_edited_channel_posts = None + new_inline_queries = None + new_chosen_inline_results = None + new_callback_queries = None + new_shipping_queries = None + new_pre_checkout_queries = None + new_polls = None + new_poll_answers = None + new_my_chat_members = None + new_chat_members = None + chat_join_request = None + for update in updates: + logger.debug('Processing updates: {0}'.format(update)) + if update.message: + if new_messages is None: new_messages = [] + new_messages.append(update.message) + if update.edited_message: + if new_edited_messages is None: new_edited_messages = [] + new_edited_messages.append(update.edited_message) + if update.channel_post: + if new_channel_posts is None: new_channel_posts = [] + new_channel_posts.append(update.channel_post) + if update.edited_channel_post: + if new_edited_channel_posts is None: new_edited_channel_posts = [] + new_edited_channel_posts.append(update.edited_channel_post) + if update.inline_query: + if new_inline_queries is None: new_inline_queries = [] + new_inline_queries.append(update.inline_query) + if update.chosen_inline_result: + if new_chosen_inline_results is None: new_chosen_inline_results = [] + new_chosen_inline_results.append(update.chosen_inline_result) + if update.callback_query: + if new_callback_queries is None: new_callback_queries = [] + new_callback_queries.append(update.callback_query) + if update.shipping_query: + if new_shipping_queries is None: new_shipping_queries = [] + new_shipping_queries.append(update.shipping_query) + if update.pre_checkout_query: + if new_pre_checkout_queries is None: new_pre_checkout_queries = [] + new_pre_checkout_queries.append(update.pre_checkout_query) + if update.poll: + if new_polls is None: new_polls = [] + new_polls.append(update.poll) + if update.poll_answer: + if new_poll_answers is None: new_poll_answers = [] + new_poll_answers.append(update.poll_answer) + if update.my_chat_member: + if new_my_chat_members is None: new_my_chat_members = [] + new_my_chat_members.append(update.my_chat_member) + if update.chat_member: + if new_chat_members is None: new_chat_members = [] + new_chat_members.append(update.chat_member) + if update.chat_join_request: + if chat_join_request is None: chat_join_request = [] + chat_join_request.append(update.chat_join_request) + + if new_messages: + await self.process_new_messages(new_messages) + if new_edited_messages: + await self.process_new_edited_messages(new_edited_messages) + if new_channel_posts: + await self.process_new_channel_posts(new_channel_posts) + if new_edited_channel_posts: + await self.process_new_edited_channel_posts(new_edited_channel_posts) + if new_inline_queries: + await self.process_new_inline_query(new_inline_queries) + if new_chosen_inline_results: + await self.process_new_chosen_inline_query(new_chosen_inline_results) + if new_callback_queries: + await self.process_new_callback_query(new_callback_queries) + if new_shipping_queries: + await self.process_new_shipping_query(new_shipping_queries) + if new_pre_checkout_queries: + await self.process_new_pre_checkout_query(new_pre_checkout_queries) + if new_polls: + await self.process_new_poll(new_polls) + if new_poll_answers: + await self.process_new_poll_answer(new_poll_answers) + if new_my_chat_members: + await self.process_new_my_chat_member(new_my_chat_members) + if new_chat_members: + await self.process_new_chat_member(new_chat_members) + if chat_join_request: + await self.process_chat_join_request(chat_join_request) + + async def process_new_messages(self, new_messages): + await self.__notify_update(new_messages) + await self._process_updates(self.message_handlers, new_messages, 'message') + + async def process_new_edited_messages(self, edited_message): + await self._process_updates(self.edited_message_handlers, edited_message, 'edited_message') + + async def process_new_channel_posts(self, channel_post): + await self._process_updates(self.channel_post_handlers, channel_post , 'channel_post') + + async def process_new_edited_channel_posts(self, edited_channel_post): + await self._process_updates(self.edited_channel_post_handlers, edited_channel_post, 'edited_channel_post') + + async def process_new_inline_query(self, new_inline_querys): + await self._process_updates(self.inline_handlers, new_inline_querys, 'inline_query') + + async def process_new_chosen_inline_query(self, new_chosen_inline_querys): + await self._process_updates(self.chosen_inline_handlers, new_chosen_inline_querys, 'chosen_inline_query') + + async def process_new_callback_query(self, new_callback_querys): + await self._process_updates(self.callback_query_handlers, new_callback_querys, 'callback_query') + + async def process_new_shipping_query(self, new_shipping_querys): + await self._process_updates(self.shipping_query_handlers, new_shipping_querys, 'shipping_query') + + async def process_new_pre_checkout_query(self, pre_checkout_querys): + await self._process_updates(self.pre_checkout_query_handlers, pre_checkout_querys, 'pre_checkout_query') + + async def process_new_poll(self, polls): + await self._process_updates(self.poll_handlers, polls, 'poll') + + async def process_new_poll_answer(self, poll_answers): + await self._process_updates(self.poll_answer_handlers, poll_answers, 'poll_answer') + + async def process_new_my_chat_member(self, my_chat_members): + await self._process_updates(self.my_chat_member_handlers, my_chat_members, 'my_chat_member') + + async def process_new_chat_member(self, chat_members): + await self._process_updates(self.chat_member_handlers, chat_members, 'chat_member') + + async def process_chat_join_request(self, chat_join_request): + await self._process_updates(self.chat_join_request_handlers, chat_join_request, 'chat_join_request') + + async def process_middlewares(self, update_type): + if self.middlewares: + middlewares = [middleware for middleware in self.middlewares if update_type in middleware.update_types] + return middlewares + return None + + async def __notify_update(self, new_messages): + if len(self.update_listener) == 0: + return + for listener in self.update_listener: + self._loop_create_task(listener(new_messages)) + + async def _test_message_handler(self, message_handler, message): + """ + Test message handler. + + :param message_handler: + :param message: + :return: + """ + for message_filter, filter_value in message_handler['filters'].items(): + if filter_value is None: + continue + + if not await self._test_filter(message_filter, filter_value, message): + return False + + return True + + def set_update_listener(self, func): + """ + Update listener is a function that gets any update. + + :param func: function that should get update. + """ + self.update_listener.append(func) + + def add_custom_filter(self, custom_filter): + """ + Create custom filter. + + custom_filter: Class with check(message) method. + """ + self.custom_filters[custom_filter.key] = custom_filter + + async def _test_filter(self, message_filter, filter_value, message): + """ + Test filters. + + :param message_filter: Filter type passed in handler + :param filter_value: Filter value passed in handler + :param message: Message to test + :return: True if filter conforms + """ + # test_cases = { + # 'content_types': lambda msg: msg.content_type in filter_value, + # 'regexp': lambda msg: msg.content_type == 'text' and re.search(filter_value, msg.text, re.IGNORECASE), + # 'commands': lambda msg: msg.content_type == 'text' and util.extract_command(msg.text) in filter_value, + # 'func': lambda msg: filter_value(msg) + # } + # return test_cases.get(message_filter, lambda msg: False)(message) + if message_filter == 'content_types': + return message.content_type in filter_value + elif message_filter == 'regexp': + return message.content_type == 'text' and re.search(filter_value, message.text, re.IGNORECASE) + elif message_filter == 'commands': + return message.content_type == 'text' and util.extract_command(message.text) in filter_value + elif message_filter == 'chat_types': + return message.chat.type in filter_value + elif message_filter == 'func': + return filter_value(message) + elif self.custom_filters and message_filter in self.custom_filters: + return await self._check_filter(message_filter,filter_value,message) + else: + return False + + async def _check_filter(self, message_filter, filter_value, message): + """ + Check up the filter. + + :param message_filter: + :param filter_value: + :param message: + :return: + """ + filter_check = self.custom_filters.get(message_filter) + if not filter_check: + return False + elif isinstance(filter_check, asyncio_filters.SimpleCustomFilter): + return filter_value == await filter_check.check(message) + elif isinstance(filter_check, asyncio_filters.AdvancedCustomFilter): + return await filter_check.check(message, filter_value) + else: + logger.error("Custom filter: wrong type. Should be SimpleCustomFilter or AdvancedCustomFilter.") + return False + + def setup_middleware(self, middleware): + """ + Setup middleware. + + :param middleware: Middleware-class. + :return: + """ + self.middlewares.append(middleware) + + def message_handler(self, commands=None, regexp=None, func=None, content_types=None, chat_types=None, **kwargs): + """ + Message handler decorator. + This decorator can be used to decorate functions that must handle certain types of messages. + All message handlers are tested in the order they were added. + + Example: + + .. code-block:: python + + bot = TeleBot('TOKEN') + + # Handles all messages which text matches regexp. + @bot.message_handler(regexp='someregexp') + async def command_help(message): + bot.send_message(message.chat.id, 'Did someone call for help?') + + # Handles messages in private chat + @bot.message_handler(chat_types=['private']) # You can add more chat types + async def command_help(message): + bot.send_message(message.chat.id, 'Private chat detected, sir!') + + # Handle all sent documents of type 'text/plain'. + @bot.message_handler(func=lambda message: message.document.mime_type == 'text/plain', + content_types=['document']) + async def command_handle_document(message): + bot.send_message(message.chat.id, 'Document received, sir!') + + # Handle all other messages. + @bot.message_handler(func=lambda message: True, content_types=['audio', 'photo', 'voice', 'video', 'document', + 'text', 'location', 'contact', 'sticker']) + async def async default_command(message): + bot.send_message(message.chat.id, "This is the async default command handler.") + + :param commands: Optional list of strings (commands to handle). + :param regexp: Optional regular expression. + :param func: Optional lambda function. The lambda receives the message to test as the first parameter. + It must return True if the command should handle the message. + :param content_types: Supported message content types. Must be a list. async defaults to ['text']. + :param chat_types: list of chat types + """ + + if content_types is None: + content_types = ["text"] + + if isinstance(commands, str): + logger.warning("message_handler: 'commands' filter should be List of strings (commands), not string.") + commands = [commands] + + if isinstance(content_types, str): + logger.warning("message_handler: 'content_types' filter should be List of strings (content types), not string.") + content_types = [content_types] + + def decorator(handler): + handler_dict = self._build_handler_dict(handler, + chat_types=chat_types, + content_types=content_types, + commands=commands, + regexp=regexp, + func=func, + **kwargs) + self.add_message_handler(handler_dict) + return handler + + return decorator + + def add_message_handler(self, handler_dict): + """ + Adds a message handler. + Note that you should use register_message_handler to add message_handler. + + :param handler_dict: + :return: + """ + self.message_handlers.append(handler_dict) + + def register_message_handler(self, callback, content_types=None, commands=None, regexp=None, func=None, chat_types=None, pass_bot=False, **kwargs): + """ + Registers message handler. + + :param callback: function to be called + :param content_types: list of content_types + :param commands: list of commands + :param regexp: + :param func: + :param chat_types: True for private chat + :param pass_bot: True if you want to get TeleBot instance in your handler + :return: decorated function + """ + if content_types is None: + content_types = ["text"] + if isinstance(commands, str): + logger.warning("register_message_handler: 'commands' filter should be List of strings (commands), not string.") + commands = [commands] + + if isinstance(content_types, str): + logger.warning("register_message_handler: 'content_types' filter should be List of strings (content types), not string.") + content_types = [content_types] + + handler_dict = self._build_handler_dict(callback, + chat_types=chat_types, + content_types=content_types, + commands=commands, + regexp=regexp, + func=func, + pass_bot=pass_bot, + **kwargs) + self.add_message_handler(handler_dict) + + def edited_message_handler(self, commands=None, regexp=None, func=None, content_types=None, chat_types=None, **kwargs): + """ + Edit message handler decorator. + + :param commands: + :param regexp: + :param func: + :param content_types: + :param chat_types: list of chat types + :param kwargs: + :return: + """ + + if content_types is None: + content_types = ["text"] + + if isinstance(commands, str): + logger.warning("edited_message_handler: 'commands' filter should be List of strings (commands), not string.") + commands = [commands] + + if isinstance(content_types, str): + logger.warning("edited_message_handler: 'content_types' filter should be List of strings (content types), not string.") + content_types = [content_types] + + def decorator(handler): + handler_dict = self._build_handler_dict(handler, + chat_types=chat_types, + content_types=content_types, + commands=commands, + regexp=regexp, + func=func, + **kwargs) + self.add_edited_message_handler(handler_dict) + return handler + + return decorator + + def add_edited_message_handler(self, handler_dict): + """ + Adds the edit message handler. + Note that you should use register_edited_message_handler to add edited_message_handler. + + :param handler_dict: + :return: + """ + self.edited_message_handlers.append(handler_dict) + + def register_edited_message_handler(self, callback, content_types=None, commands=None, regexp=None, func=None, chat_types=None, pass_bot=False, **kwargs): + """ + Registers edited message handler. + + :param pass_bot: + :param callback: function to be called + :param content_types: list of content_types + :param commands: list of commands + :param regexp: + :param func: + :param chat_types: True for private chat + :return: decorated function + """ + if isinstance(commands, str): + logger.warning("register_edited_message_handler: 'commands' filter should be List of strings (commands), not string.") + commands = [commands] + + if isinstance(content_types, str): + logger.warning("register_edited_message_handler: 'content_types' filter should be List of strings (content types), not string.") + content_types = [content_types] + + handler_dict = self._build_handler_dict(callback, + chat_types=chat_types, + content_types=content_types, + commands=commands, + regexp=regexp, + func=func, + pass_bot=pass_bot, + **kwargs) + self.add_edited_message_handler(handler_dict) + + + def channel_post_handler(self, commands=None, regexp=None, func=None, content_types=None, **kwargs): + """ + Channel post handler decorator. + + :param commands: + :param regexp: + :param func: + :param content_types: + :param kwargs: + :return: + """ + if content_types is None: + content_types = ["text"] + + if isinstance(commands, str): + logger.warning("channel_post_handler: 'commands' filter should be List of strings (commands), not string.") + commands = [commands] + + if isinstance(content_types, str): + logger.warning("channel_post_handler: 'content_types' filter should be List of strings (content types), not string.") + content_types = [content_types] + + def decorator(handler): + handler_dict = self._build_handler_dict(handler, + content_types=content_types, + commands=commands, + regexp=regexp, + func=func, + **kwargs) + self.add_channel_post_handler(handler_dict) + return handler + + return decorator + + def add_channel_post_handler(self, handler_dict): + """ + Adds channel post handler. + Note that you should use register_channel_post_handler to add channel_post_handler. + + :param handler_dict: + :return: + """ + self.channel_post_handlers.append(handler_dict) + + def register_channel_post_handler(self, callback, content_types=None, commands=None, regexp=None, func=None, pass_bot=False, **kwargs): + """ + Registers channel post message handler. + + :param pass_bot: + :param callback: function to be called + :param content_types: list of content_types + :param commands: list of commands + :param regexp: + :param func: + :return: decorated function + """ + if isinstance(commands, str): + logger.warning("register_channel_post_handler: 'commands' filter should be List of strings (commands), not string.") + commands = [commands] + + if isinstance(content_types, str): + logger.warning("register_channel_post_handler: 'content_types' filter should be List of strings (content types), not string.") + content_types = [content_types] + + handler_dict = self._build_handler_dict(callback, + content_types=content_types, + commands=commands, + regexp=regexp, + func=func, + pass_bot=pass_bot, + **kwargs) + self.add_channel_post_handler(handler_dict) + + def edited_channel_post_handler(self, commands=None, regexp=None, func=None, content_types=None, **kwargs): + """ + Edit channel post handler decorator. + + :param commands: + :param regexp: + :param func: + :param content_types: + :param kwargs: + :return: + """ + if content_types is None: + content_types = ["text"] + + if isinstance(commands, str): + logger.warning("edited_channel_post_handler: 'commands' filter should be List of strings (commands), not string.") + commands = [commands] + + if isinstance(content_types, str): + logger.warning("edited_channel_post_handler: 'content_types' filter should be List of strings (content types), not string.") + content_types = [content_types] + + def decorator(handler): + handler_dict = self._build_handler_dict(handler, + content_types=content_types, + commands=commands, + regexp=regexp, + func=func, + **kwargs) + self.add_edited_channel_post_handler(handler_dict) + return handler + + return decorator + + def add_edited_channel_post_handler(self, handler_dict): + """ + Adds the edit channel post handler. + Note that you should use register_edited_channel_post_handler to add edited_channel_post_handler. + + :param handler_dict: + :return: + """ + self.edited_channel_post_handlers.append(handler_dict) + + def register_edited_channel_post_handler(self, callback, content_types=None, commands=None, regexp=None, func=None, pass_bot=False, **kwargs): + """ + Registers edited channel post message handler. + + :param pass_bot: + :param callback: function to be called + :param content_types: list of content_types + :param commands: list of commands + :param regexp: + :param func: + :return: decorated function + """ + if isinstance(commands, str): + logger.warning("register_edited_channel_post_handler: 'commands' filter should be List of strings (commands), not string.") + commands = [commands] + + if isinstance(content_types, str): + logger.warning("register_edited_channel_post_handler: 'content_types' filter should be List of strings (content types), not string.") + content_types = [content_types] + + handler_dict = self._build_handler_dict(callback, + content_types=content_types, + commands=commands, + regexp=regexp, + func=func, + pass_bot=pass_bot, + **kwargs) + self.add_edited_channel_post_handler(handler_dict) + + def inline_handler(self, func, **kwargs): + """ + Inline call handler decorator. + + :param func: + :param kwargs: + :return: + """ + + def decorator(handler): + handler_dict = self._build_handler_dict(handler, func=func, **kwargs) + self.add_inline_handler(handler_dict) + return handler + + return decorator + + def add_inline_handler(self, handler_dict): + """ + Adds inline call handler. + Note that you should use register_inline_handler to add inline_handler. + + :param handler_dict: + :return: + """ + self.inline_handlers.append(handler_dict) + + def register_inline_handler(self, callback, func, pass_bot=False, **kwargs): + """ + Registers inline handler. + + :param pass_bot: + :param callback: function to be called + :param func: + :return: decorated function + """ + handler_dict = self._build_handler_dict(callback, func=func, pass_bot=pass_bot, **kwargs) + self.add_inline_handler(handler_dict) + + def chosen_inline_handler(self, func, **kwargs): + """ + + Description: TBD + + :param func: + :param kwargs: + :return: + """ + + def decorator(handler): + handler_dict = self._build_handler_dict(handler, func=func, **kwargs) + self.add_chosen_inline_handler(handler_dict) + return handler + + return decorator + + def add_chosen_inline_handler(self, handler_dict): + """ + Description: TBD + Note that you should use register_chosen_inline_handler to add chosen_inline_handler. + + :param handler_dict: + :return: + """ + self.chosen_inline_handlers.append(handler_dict) + + def register_chosen_inline_handler(self, callback, func, pass_bot=False, **kwargs): + """ + Registers chosen inline handler. + + :param pass_bot: + :param callback: function to be called + :param func: + :return: decorated function + """ + handler_dict = self._build_handler_dict(callback, func=func, pass_bot=pass_bot, **kwargs) + self.add_chosen_inline_handler(handler_dict) + + def callback_query_handler(self, func, **kwargs): + """ + Callback request handler decorator. + + :param func: + :param kwargs: + :return: + """ + + def decorator(handler): + handler_dict = self._build_handler_dict(handler, func=func, **kwargs) + self.add_callback_query_handler(handler_dict) + return handler + + return decorator + + def add_callback_query_handler(self, handler_dict): + """ + Adds a callback request handler. + Note that you should use register_callback_query_handler to add callback_query_handler. + + :param handler_dict: + :return: + """ + self.callback_query_handlers.append(handler_dict) + + def register_callback_query_handler(self, callback, func, pass_bot=False, **kwargs): + """ + Registers callback query handler. + + :param pass_bot: + :param callback: function to be called + :param func: + :return: decorated function + """ + handler_dict = self._build_handler_dict(callback, func=func, pass_bot=pass_bot, **kwargs) + self.add_callback_query_handler(handler_dict) + + def shipping_query_handler(self, func, **kwargs): + """ + Shipping request handler. + + :param func: + :param kwargs: + :return: + """ + + def decorator(handler): + handler_dict = self._build_handler_dict(handler, func=func, **kwargs) + self.add_shipping_query_handler(handler_dict) + return handler + + return decorator + + def add_shipping_query_handler(self, handler_dict): + """ + Adds a shipping request handler. + Note that you should use register_shipping_query_handler to add shipping_query_handler. + + :param handler_dict: + :return: + """ + self.shipping_query_handlers.append(handler_dict) + + def register_shipping_query_handler(self, callback, func, pass_bot=False, **kwargs): + """ + Registers shipping query handler. + + :param pass_bot: + :param callback: function to be called + :param func: + :return: decorated function + """ + handler_dict = self._build_handler_dict(callback, func=func, pass_bot=pass_bot, **kwargs) + self.add_shipping_query_handler(handler_dict) + + def pre_checkout_query_handler(self, func, **kwargs): + """ + Pre-checkout request handler. + + :param func: + :param kwargs: + :return: + """ + + def decorator(handler): + handler_dict = self._build_handler_dict(handler, func=func, **kwargs) + self.add_pre_checkout_query_handler(handler_dict) + return handler + + return decorator + + def add_pre_checkout_query_handler(self, handler_dict): + """ + Adds a pre-checkout request handler. + Note that you should use register_pre_checkout_query_handler to add pre_checkout_query_handler. + + :param handler_dict: + :return: + """ + self.pre_checkout_query_handlers.append(handler_dict) + + def register_pre_checkout_query_handler(self, callback, func, pass_bot=False, **kwargs): + """ + Registers pre-checkout request handler. + + :param pass_bot: + :param callback: function to be called + :param func: + :return: decorated function + """ + handler_dict = self._build_handler_dict(callback, func=func, pass_bot=pass_bot, **kwargs) + self.add_pre_checkout_query_handler(handler_dict) + + def poll_handler(self, func, **kwargs): + """ + Poll request handler. + + :param func: + :param kwargs: + :return: + """ + + def decorator(handler): + handler_dict = self._build_handler_dict(handler, func=func, **kwargs) + self.add_poll_handler(handler_dict) + return handler + + return decorator + + def add_poll_handler(self, handler_dict): + """ + Adds a poll request handler. + Note that you should use register_poll_handler to add poll_handler. + + :param handler_dict: + :return: + """ + self.poll_handlers.append(handler_dict) + + def register_poll_handler(self, callback, func, pass_bot=False, **kwargs): + """ + Registers poll handler. + + :param pass_bot: + :param callback: function to be called + :param func: + :return: decorated function + """ + handler_dict = self._build_handler_dict(callback, func=func, pass_bot=pass_bot, **kwargs) + self.add_poll_handler(handler_dict) + + def poll_answer_handler(self, func=None, **kwargs): + """ + Poll_answer request handler. + + :param func: + :param kwargs: + :return: + """ + + def decorator(handler): + handler_dict = self._build_handler_dict(handler, func=func, **kwargs) + self.add_poll_answer_handler(handler_dict) + return handler + + return decorator + + def add_poll_answer_handler(self, handler_dict): + """ + Adds a poll_answer request handler. + Note that you should use register_poll_answer_handler to add poll_answer_handler. + + :param handler_dict: + :return: + """ + self.poll_answer_handlers.append(handler_dict) + + def register_poll_answer_handler(self, callback, func, pass_bot=False, **kwargs): + """ + Registers poll answer handler. + + :param pass_bot: + :param callback: function to be called + :param func: + :return: decorated function + """ + handler_dict = self._build_handler_dict(callback, func=func, pass_bot=pass_bot, **kwargs) + self.add_poll_answer_handler(handler_dict) + + def my_chat_member_handler(self, func=None, **kwargs): + """ + my_chat_member handler. + + :param func: + :param kwargs: + :return: + """ + + def decorator(handler): + handler_dict = self._build_handler_dict(handler, func=func, **kwargs) + self.add_my_chat_member_handler(handler_dict) + return handler + + return decorator + + def add_my_chat_member_handler(self, handler_dict): + """ + Adds a my_chat_member handler. + Note that you should use register_my_chat_member_handler to add my_chat_member_handler. + + :param handler_dict: + :return: + """ + self.my_chat_member_handlers.append(handler_dict) + + def register_my_chat_member_handler(self, callback, func=None, pass_bot=False, **kwargs): + """ + Registers my chat member handler. + + :param pass_bot: + :param callback: function to be called + :param func: + :return: decorated function + """ + handler_dict = self._build_handler_dict(callback, func=func, pass_bot=pass_bot, **kwargs) + self.add_my_chat_member_handler(handler_dict) + + def chat_member_handler(self, func=None, **kwargs): + """ + chat_member handler. + + :param func: + :param kwargs: + :return: + """ + + def decorator(handler): + handler_dict = self._build_handler_dict(handler, func=func, **kwargs) + self.add_chat_member_handler(handler_dict) + return handler + + return decorator + + def add_chat_member_handler(self, handler_dict): + """ + Adds a chat_member handler. + Note that you should use register_chat_member_handler to add chat_member_handler. + + :param handler_dict: + :return: + """ + self.chat_member_handlers.append(handler_dict) + + def register_chat_member_handler(self, callback, func=None, pass_bot=False, **kwargs): + """ + Registers chat member handler. + + :param pass_bot: + :param callback: function to be called + :param func: + :return: decorated function + """ + handler_dict = self._build_handler_dict(callback, func=func, pass_bot=pass_bot, **kwargs) + self.add_chat_member_handler(handler_dict) + + def chat_join_request_handler(self, func=None, **kwargs): + """ + chat_join_request handler. + + :param func: + :param kwargs: + :return: + """ + + def decorator(handler): + handler_dict = self._build_handler_dict(handler, func=func, **kwargs) + self.add_chat_join_request_handler(handler_dict) + return handler + + return decorator + + def add_chat_join_request_handler(self, handler_dict): + """ + Adds a chat_join_request handler. + Note that you should use register_chat_join_request_handler to add chat_join_request_handler. + + :param handler_dict: + :return: + """ + self.chat_join_request_handlers.append(handler_dict) + + def register_chat_join_request_handler(self, callback, func=None, pass_bot=False, **kwargs): + """ + Registers chat join request handler. + + :param pass_bot: + :param callback: function to be called + :param func: + :return: decorated function + """ + handler_dict = self._build_handler_dict(callback, func=func, pass_bot=pass_bot, **kwargs) + self.add_chat_join_request_handler(handler_dict) + + @staticmethod + def _build_handler_dict(handler, pass_bot=False, **filters): + """ + Builds a dictionary for a handler. + + :param handler: + :param filters: + :return: + """ + return { + 'function': handler, + 'pass_bot': pass_bot, + 'filters': {ftype: fvalue for ftype, fvalue in filters.items() if fvalue is not None} + # Remove None values, they are skipped in _test_filter anyway + #'filters': filters + } + + async def skip_updates(self): + """ + Skip existing updates. + Only last update will remain on server. + """ + await self.get_updates(-1) + return True + + # all methods begin here + + async def get_me(self) -> types.User: + """ + Returns basic information about the bot in form of a User object. + + Telegram documentation: https://core.telegram.org/bots/api#getme + """ + result = await asyncio_helper.get_me(self.token) + return types.User.de_json(result) + + async def get_file(self, file_id: str) -> types.File: + """ + Use this method to get basic info about a file and prepare it for downloading. + For the moment, bots can download files of up to 20MB in size. + On success, a File object is returned. + It is guaranteed that the link will be valid for at least 1 hour. + When the link expires, a new one can be requested by calling get_file again. + + Telegram documentation: https://core.telegram.org/bots/api#getfile + + :param file_id: + """ + return types.File.de_json(await asyncio_helper.get_file(self.token, file_id)) + + async def get_file_url(self, file_id: str) -> str: + + return await asyncio_helper.get_file_url(self.token, file_id) + + async def download_file(self, file_path: str) -> bytes: + return await asyncio_helper.download_file(self.token, file_path) + + async def log_out(self) -> bool: + """ + Use this method to log out from the cloud Bot API server before launching the bot locally. + You MUST log out the bot before running it locally, otherwise there is no guarantee + that the bot will receive updates. + After a successful call, you can immediately log in on a local server, + but will not be able to log in back to the cloud Bot API server for 10 minutes. + Returns True on success. + + Telegram documentation: https://core.telegram.org/bots/api#logout + """ + return await asyncio_helper.log_out(self.token) + + async def close(self) -> bool: + """ + Use this method to close the bot instance before moving it from one local server to another. + You need to delete the webhook before calling this method to ensure that the bot isn't launched again + after server restart. + The method will return error 429 in the first 10 minutes after the bot is launched. + Returns True on success. + + Telegram documentation: https://core.telegram.org/bots/api#close + """ + return await asyncio_helper.close(self.token) + + def enable_saving_states(self, filename="./.state-save/states.pkl"): + """ + Enable saving states (by default saving disabled) + + :param filename: Filename of saving file + """ + + self.current_states = StatePickleStorage(file_path=filename) + + async def set_webhook(self, url=None, certificate=None, max_connections=None, allowed_updates=None, ip_address=None, + drop_pending_updates = None, timeout=None): + """ + Use this method to specify a url and receive incoming updates via an outgoing webhook. Whenever there is an + update for the bot, we will send an HTTPS POST request to the specified url, + containing a JSON-serialized Update. + In case of an unsuccessful request, we will give up after a reasonable amount of attempts. + Returns True on success. + + Telegram documentation: https://core.telegram.org/bots/api#setwebhook + + :param url: HTTPS url to send updates to. Use an empty string to remove webhook integration + :param certificate: Upload your public key certificate so that the root certificate in use can be checked. + See our self-signed guide for details. + :param max_connections: Maximum allowed number of simultaneous HTTPS connections to the webhook + for update delivery, 1-100. Defaults to 40. Use lower values to limit the load on your bot's server, + and higher values to increase your bot's throughput. + :param allowed_updates: A JSON-serialized list of the update types you want your bot to receive. + For example, specify [“message”, “edited_channel_post”, “callback_query”] to only receive updates + of these types. See Update for a complete list of available update types. + Specify an empty list to receive all updates regardless of type (default). + If not specified, the previous setting will be used. + :param ip_address: The fixed IP address which will be used to send webhook requests instead of the IP address + resolved through DNS + :param drop_pending_updates: Pass True to drop all pending updates + :param timeout: Integer. Request connection timeout + :return: + """ + return await asyncio_helper.set_webhook(self.token, url, certificate, max_connections, allowed_updates, ip_address, + drop_pending_updates, timeout) + + + + async def delete_webhook(self, drop_pending_updates=None, timeout=None): + """ + Use this method to remove webhook integration if you decide to switch back to getUpdates. + + Telegram documentation: https://core.telegram.org/bots/api#deletewebhook + + :param drop_pending_updates: Pass True to drop all pending updates + :param timeout: Integer. Request connection timeout + :return: bool + """ + return await asyncio_helper.delete_webhook(self.token, drop_pending_updates, timeout) + + async def remove_webhook(self): + """ + Alternative for delete_webhook but uses set_webhook + """ + await self.set_webhook() + + async def get_webhook_info(self, timeout=None): + """ + Use this method to get current webhook status. Requires no parameters. + If the bot is using getUpdates, will return an object with the url field empty. + + Telegram documentation: https://core.telegram.org/bots/api#getwebhookinfo + + :param timeout: Integer. Request connection timeout + :return: On success, returns a WebhookInfo object. + """ + result = await asyncio_helper.get_webhook_info(self.token, timeout) + return types.WebhookInfo.de_json(result) + + async def get_user_profile_photos(self, user_id: int, offset: Optional[int]=None, + limit: Optional[int]=None) -> types.UserProfilePhotos: + """ + Retrieves the user profile photos of the person with 'user_id' + + Telegram documentation: https://core.telegram.org/bots/api#getuserprofilephotos + + :param user_id: + :param offset: + :param limit: + :return: API reply. + """ + result = await asyncio_helper.get_user_profile_photos(self.token, user_id, offset, limit) + return types.UserProfilePhotos.de_json(result) + + async def get_chat(self, chat_id: Union[int, str]) -> types.Chat: + """ + Use this method to get up to date information about the chat (current name of the user for one-on-one + conversations, current username of a user, group or channel, etc.). Returns a Chat object on success. + + Telegram documentation: https://core.telegram.org/bots/api#getchat + + :param chat_id: + :return: + """ + result = await asyncio_helper.get_chat(self.token, chat_id) + return types.Chat.de_json(result) + + async def leave_chat(self, chat_id: Union[int, str]) -> bool: + """ + Use this method for your bot to leave a group, supergroup or channel. Returns True on success. + + Telegram documentation: https://core.telegram.org/bots/api#leavechat + + :param chat_id: + :return: + """ + result = await asyncio_helper.leave_chat(self.token, chat_id) + return result + + async def get_chat_administrators(self, chat_id: Union[int, str]) -> List[types.ChatMember]: + """ + Use this method to get a list of administrators in a chat. + On success, returns an Array of ChatMember objects that contains + information about all chat administrators except other bots. + + Telegram documentation: https://core.telegram.org/bots/api#getchatadministrators + + :param chat_id: Unique identifier for the target chat or username of the target supergroup or channel (in the format @channelusername) + :return: API reply. + """ + result = await asyncio_helper.get_chat_administrators(self.token, chat_id) + return [types.ChatMember.de_json(r) for r in result] + + async def get_chat_members_count(self, chat_id: Union[int, str]) -> int: + """ + This function is deprecated. Use `get_chat_member_count` instead + """ + logger.info('get_chat_members_count is deprecated. Use get_chat_member_count instead.') + result = await asyncio_helper.get_chat_member_count(self.token, chat_id) + return result + + async def get_chat_member_count(self, chat_id: Union[int, str]) -> int: + """ + Use this method to get the number of members in a chat. Returns Int on success. + + Telegram documentation: https://core.telegram.org/bots/api#getchatmemberscount + + :param chat_id: + :return: + """ + result = await asyncio_helper.get_chat_member_count(self.token, chat_id) + return result + + async def set_chat_sticker_set(self, chat_id: Union[int, str], sticker_set_name: str) -> types.StickerSet: + """ + Use this method to set a new group sticker set for a supergroup. The bot must be an administrator + in the chat for this to work and must have the appropriate admin rights. + Use the field can_set_sticker_set optionally returned in getChat requests to check + if the bot can use this method. Returns True on success. + + Telegram documentation: https://core.telegram.org/bots/api#setchatstickerset + + :param chat_id: Unique identifier for the target chat or username of the target supergroup (in the format @supergroupusername) + :param sticker_set_name: Name of the sticker set to be set as the group sticker set + :return: API reply. + """ + result = await asyncio_helper.set_chat_sticker_set(self.token, chat_id, sticker_set_name) + return result + + async def delete_chat_sticker_set(self, chat_id: Union[int, str]) -> bool: + """ + Use this method to delete a group sticker set from a supergroup. The bot must be an administrator in the chat + for this to work and must have the appropriate admin rights. Use the field can_set_sticker_set + optionally returned in getChat requests to check if the bot can use this method. Returns True on success. + + Telegram documentation: https://core.telegram.org/bots/api#deletechatstickerset + + :param chat_id: Unique identifier for the target chat or username of the target supergroup (in the format @supergroupusername) + :return: API reply. + """ + result = await asyncio_helper.delete_chat_sticker_set(self.token, chat_id) + return result + + async def get_chat_member(self, chat_id: Union[int, str], user_id: int) -> types.ChatMember: + """ + Use this method to get information about a member of a chat. Returns a ChatMember object on success. + + Telegram documentation: https://core.telegram.org/bots/api#getchatmember + + :param chat_id: + :param user_id: + :return: API reply. + """ + result = await asyncio_helper.get_chat_member(self.token, chat_id, user_id) + return types.ChatMember.de_json(result) + + + + async def send_message( + self, chat_id: Union[int, str], text: str, + parse_mode: Optional[str]=None, + entities: Optional[List[types.MessageEntity]]=None, + disable_web_page_preview: Optional[bool]=None, + disable_notification: Optional[bool]=None, + protect_content: Optional[bool]=None, + reply_to_message_id: Optional[int]=None, + allow_sending_without_reply: Optional[bool]=None, + reply_markup: Optional[REPLY_MARKUP_TYPES]=None, + timeout: Optional[int]=None) -> types.Message: + """ + Use this method to send text messages. + + Warning: Do not send more than about 4000 characters each message, otherwise you'll risk an HTTP 414 error. + If you must send more than 4000 characters, + use the `split_string` or `smart_split` function in util.py. + + Telegram documentation: https://core.telegram.org/bots/api#sendmessage + + :param chat_id: + :param text: + :param disable_web_page_preview: + :param reply_to_message_id: + :param reply_markup: + :param parse_mode: + :param disable_notification: Boolean, Optional. Sends the message silently. + :param timeout: + :param entities: + :param allow_sending_without_reply: + :param protect_content: + :return: API reply. + """ + parse_mode = self.parse_mode if (parse_mode is None) else parse_mode + + return types.Message.de_json( + await asyncio_helper.send_message( + self.token, chat_id, text, disable_web_page_preview, reply_to_message_id, + reply_markup, parse_mode, disable_notification, timeout, + entities, allow_sending_without_reply, protect_content)) + + async def forward_message( + self, chat_id: Union[int, str], from_chat_id: Union[int, str], + message_id: int, disable_notification: Optional[bool]=None, + protect_content: Optional[bool]=None, + timeout: Optional[int]=None) -> types.Message: + """ + Use this method to forward messages of any kind. + + Telegram documentation: https://core.telegram.org/bots/api#forwardmessage + + :param disable_notification: + :param chat_id: which chat to forward + :param from_chat_id: which chat message from + :param message_id: message id + :param protect_content: + :param timeout: + :return: API reply. + """ + return types.Message.de_json( + await asyncio_helper.forward_message(self.token, chat_id, from_chat_id, message_id, disable_notification, timeout, protect_content)) + + async def copy_message( + self, chat_id: Union[int, str], + from_chat_id: Union[int, str], + message_id: int, + caption: Optional[str]=None, + parse_mode: Optional[str]=None, + caption_entities: Optional[List[types.MessageEntity]]=None, + disable_notification: Optional[bool]=None, + protect_content: Optional[bool]=None, + reply_to_message_id: Optional[int]=None, + allow_sending_without_reply: Optional[bool]=None, + reply_markup: Optional[REPLY_MARKUP_TYPES]=None, + timeout: Optional[int]=None) -> int: + """ + Use this method to copy messages of any kind. + + Telegram documentation: https://core.telegram.org/bots/api#copymessage + + :param chat_id: which chat to forward + :param from_chat_id: which chat message from + :param message_id: message id + :param caption: + :param parse_mode: + :param caption_entities: + :param disable_notification: + :param reply_to_message_id: + :param allow_sending_without_reply: + :param reply_markup: + :param timeout: + :param protect_content: + :return: API reply. + """ + return types.MessageID.de_json( + await asyncio_helper.copy_message(self.token, chat_id, from_chat_id, message_id, caption, parse_mode, caption_entities, + disable_notification, reply_to_message_id, allow_sending_without_reply, reply_markup, + timeout, protect_content)) + + async def delete_message(self, chat_id: Union[int, str], message_id: int, + timeout: Optional[int]=None) -> bool: + """ + Use this method to delete message. Returns True on success. + + Telegram documentation: https://core.telegram.org/bots/api#deletemessage + + :param chat_id: in which chat to delete + :param message_id: which message to delete + :param timeout: + :return: API reply. + """ + return await asyncio_helper.delete_message(self.token, chat_id, message_id, timeout) + + async def send_dice( + self, chat_id: Union[int, str], + emoji: Optional[str]=None, disable_notification: Optional[bool]=None, + reply_to_message_id: Optional[int]=None, + reply_markup: Optional[REPLY_MARKUP_TYPES]=None, + timeout: Optional[int]=None, + allow_sending_without_reply: Optional[bool]=None, + protect_content: Optional[bool]=None) -> types.Message: + """ + Use this method to send dices. + + Telegram documentation: https://core.telegram.org/bots/api#senddice + + :param chat_id: + :param emoji: + :param disable_notification: + :param reply_to_message_id: + :param reply_markup: + :param timeout: + :param allow_sending_without_reply: + :param protect_content: + :return: Message + """ + return types.Message.de_json( + await asyncio_helper.send_dice( + self.token, chat_id, emoji, disable_notification, reply_to_message_id, + reply_markup, timeout, allow_sending_without_reply, protect_content) + ) + + async def send_photo( + self, chat_id: Union[int, str], photo: Union[Any, str], + caption: Optional[str]=None, parse_mode: Optional[str]=None, + caption_entities: Optional[List[types.MessageEntity]]=None, + disable_notification: Optional[bool]=None, + protect_content: Optional[bool]=None, + reply_to_message_id: Optional[int]=None, + allow_sending_without_reply: Optional[bool]=None, + reply_markup: Optional[REPLY_MARKUP_TYPES]=None, + timeout: Optional[int]=None,) -> types.Message: + """ + Use this method to send photos. + + Telegram documentation: https://core.telegram.org/bots/api#sendphoto + + :param chat_id: + :param photo: + :param caption: + :param parse_mode: + :param disable_notification: + :param reply_to_message_id: + :param reply_markup: + :param timeout: + :param caption_entities: + :param allow_sending_without_reply: + :param protect_content: + :return: API reply. + """ + parse_mode = self.parse_mode if (parse_mode is None) else parse_mode + + return types.Message.de_json( + await asyncio_helper.send_photo( + self.token, chat_id, photo, caption, reply_to_message_id, reply_markup, + parse_mode, disable_notification, timeout, caption_entities, + allow_sending_without_reply, protect_content)) + + async def send_audio( + self, chat_id: Union[int, str], audio: Union[Any, str], + caption: Optional[str]=None, duration: Optional[int]=None, + performer: Optional[str]=None, title: Optional[str]=None, + reply_to_message_id: Optional[int]=None, + reply_markup: Optional[REPLY_MARKUP_TYPES]=None, + parse_mode: Optional[str]=None, + disable_notification: Optional[bool]=None, + timeout: Optional[int]=None, + thumb: Optional[Union[Any, str]]=None, + caption_entities: Optional[List[types.MessageEntity]]=None, + allow_sending_without_reply: Optional[bool]=None, + protect_content: Optional[bool]=None) -> types.Message: + """ + Use this method to send audio files, if you want Telegram clients to display them in the music player. + Your audio must be in the .mp3 format. + + Telegram documentation: https://core.telegram.org/bots/api#sendaudio + + :param chat_id: Unique identifier for the message recipient + :param audio: Audio file to send. + :param caption: + :param duration: Duration of the audio in seconds + :param performer: Performer + :param title: Track name + :param reply_to_message_id: If the message is a reply, ID of the original message + :param reply_markup: + :param parse_mode: + :param disable_notification: + :param timeout: + :param thumb: + :param caption_entities: + :param allow_sending_without_reply: + :param protect_content: + :return: Message + """ + parse_mode = self.parse_mode if (parse_mode is None) else parse_mode + + return types.Message.de_json( + await asyncio_helper.send_audio( + self.token, chat_id, audio, caption, duration, performer, title, reply_to_message_id, + reply_markup, parse_mode, disable_notification, timeout, thumb, + caption_entities, allow_sending_without_reply, protect_content)) + + async def send_voice( + self, chat_id: Union[int, str], voice: Union[Any, str], + caption: Optional[str]=None, duration: Optional[int]=None, + reply_to_message_id: Optional[int]=None, + reply_markup: Optional[REPLY_MARKUP_TYPES]=None, + parse_mode: Optional[str]=None, + disable_notification: Optional[bool]=None, + timeout: Optional[int]=None, + caption_entities: Optional[List[types.MessageEntity]]=None, + allow_sending_without_reply: Optional[bool]=None, + protect_content: Optional[bool]=None) -> types.Message: + """ + Use this method to send audio files, if you want Telegram clients to display the file + as a playable voice message. + + Telegram documentation: https://core.telegram.org/bots/api#sendvoice + + :param chat_id: Unique identifier for the message recipient. + :param voice: + :param caption: + :param duration: Duration of sent audio in seconds + :param reply_to_message_id: + :param reply_markup: + :param parse_mode: + :param disable_notification: + :param timeout: + :param caption_entities: + :param allow_sending_without_reply: + :param protect_content: + :return: Message + """ + parse_mode = self.parse_mode if (parse_mode is None) else parse_mode + + return types.Message.de_json( + await asyncio_helper.send_voice( + self.token, chat_id, voice, caption, duration, reply_to_message_id, reply_markup, + parse_mode, disable_notification, timeout, caption_entities, + allow_sending_without_reply, protect_content)) + + async def send_document( + self, chat_id: Union[int, str], document: Union[Any, str], + reply_to_message_id: Optional[int]=None, + caption: Optional[str]=None, + reply_markup: Optional[REPLY_MARKUP_TYPES]=None, + parse_mode: Optional[str]=None, + disable_notification: Optional[bool]=None, + timeout: Optional[int]=None, + thumb: Optional[Union[Any, str]]=None, + caption_entities: Optional[List[types.MessageEntity]]=None, + allow_sending_without_reply: Optional[bool]=None, + visible_file_name: Optional[str]=None, + disable_content_type_detection: Optional[bool]=None, + data: Optional[Union[Any, str]]=None, + protect_content: Optional[bool]=None) -> types.Message: + """ + Use this method to send general files. + + Telegram documentation: https://core.telegram.org/bots/api#senddocument + + :param chat_id: Unique identifier for the target chat or username of the target channel (in the format @channelusername) + :param document: (document) File to send. Pass a file_id as String to send a file that exists on the Telegram servers (recommended), pass an HTTP URL as a String for Telegram to get a file from the Internet, or upload a new one using multipart/form-data + :param reply_to_message_id: If the message is a reply, ID of the original message + :param caption: Document caption (may also be used when resending documents by file_id), 0-1024 characters after entities parsing + :param reply_markup: + :param parse_mode: Mode for parsing entities in the document caption + :param disable_notification: Sends the message silently. Users will receive a notification with no sound. + :param timeout: + :param thumb: InputFile or String : Thumbnail of the file sent; can be ignored if thumbnail generation for the file is supported server-side. The thumbnail should be in JPEG format and less than 200 kB in size. A thumbnail's width and height should not exceed 320. Ignored if the file is not uploaded using multipart/form-data. Thumbnails can't be reused and can be only uploaded as a new file, so you can pass “attach://” if the thumbnail was uploaded using multipart/form-data under + :param caption_entities: + :param allow_sending_without_reply: + :param visible_file_name: allows to async define file name that will be visible in the Telegram instead of original file name + :param disable_content_type_detection: Disables automatic server-side content type detection for files uploaded using multipart/form-data + :param data: function typo compatibility: do not use it + :param protect_content: + :return: API reply. + """ + parse_mode = self.parse_mode if (parse_mode is None) else parse_mode + if data and not(document): + # function typo miss compatibility + document = data + + return types.Message.de_json( + await asyncio_helper.send_data( + self.token, chat_id, document, 'document', + reply_to_message_id = reply_to_message_id, reply_markup = reply_markup, parse_mode = parse_mode, + disable_notification = disable_notification, timeout = timeout, caption = caption, thumb = thumb, + caption_entities = caption_entities, allow_sending_without_reply = allow_sending_without_reply, + disable_content_type_detection = disable_content_type_detection, visible_file_name = visible_file_name, protect_content = protect_content)) + + async def send_sticker( + self, chat_id: Union[int, str], sticker: Union[Any, str], + reply_to_message_id: Optional[int]=None, + reply_markup: Optional[REPLY_MARKUP_TYPES]=None, + disable_notification: Optional[bool]=None, + timeout: Optional[int]=None, + allow_sending_without_reply: Optional[bool]=None, + protect_content: Optional[bool]=None, + data: Union[Any, str]=None) -> types.Message: + """ + Use this method to send .webp stickers. + + Telegram documentation: https://core.telegram.org/bots/api#sendsticker + + :param chat_id: + :param sticker: + :param reply_to_message_id: + :param reply_markup: + :param disable_notification: to disable the notification + :param timeout: timeout + :param allow_sending_without_reply: + :param protect_content: + :param data: deprecated, for backward compatibility + :return: API reply. + """ + if data and not(sticker): + # function typo miss compatibility + sticker = data + return types.Message.de_json( + await asyncio_helper.send_data( + self.token, chat_id, sticker, 'sticker', + reply_to_message_id=reply_to_message_id, reply_markup=reply_markup, + disable_notification=disable_notification, timeout=timeout, + allow_sending_without_reply=allow_sending_without_reply, protect_content=protect_content)) + + async def send_video( + self, chat_id: Union[int, str], video: Union[Any, str], + duration: Optional[int]=None, + width: Optional[int]=None, + height: Optional[int]=None, + thumb: Optional[Union[Any, str]]=None, + caption: Optional[str]=None, + parse_mode: Optional[str]=None, + caption_entities: Optional[List[types.MessageEntity]]=None, + supports_streaming: Optional[bool]=None, + disable_notification: Optional[bool]=None, + protect_content: Optional[bool]=None, + reply_to_message_id: Optional[int]=None, + allow_sending_without_reply: Optional[bool]=None, + reply_markup: Optional[REPLY_MARKUP_TYPES]=None, + timeout: Optional[int]=None, + data: Optional[Union[Any, str]]=None) -> types.Message: + """ + Use this method to send video files, Telegram clients support mp4 videos (other formats may be sent as Document). + + Telegram documentation: https://core.telegram.org/bots/api#sendvideo + + :param chat_id: Unique identifier for the target chat or username of the target channel (in the format @channelusername) + :param video: Video to send. You can either pass a file_id as String to resend a video that is already on the Telegram servers, or upload a new video file using multipart/form-data. + :param duration: Duration of sent video in seconds + :param width: Video width + :param height: Video height + :param thumb: Thumbnail of the file sent; can be ignored if thumbnail generation for the file is supported server-side. The thumbnail should be in JPEG format and less than 200 kB in size. A thumbnail's width and height should not exceed 320. Ignored if the file is not uploaded using multipart/form-data. Thumbnails can't be reused and can be only uploaded as a new file, so you can pass “attach://” if the thumbnail was uploaded using multipart/form-data under . + :param caption: Video caption (may also be used when resending videos by file_id), 0-1024 characters after entities parsing + :param parse_mode: Mode for parsing entities in the video caption + :param caption_entities: + :param supports_streaming: Pass True, if the uploaded video is suitable for streaming + :param disable_notification: Sends the message silently. Users will receive a notification with no sound. + :param protect_content: + :param reply_to_message_id: If the message is a reply, ID of the original message + :param allow_sending_without_reply: + :param reply_markup: + :param timeout: + :param data: deprecated, for backward compatibility + """ + parse_mode = self.parse_mode if (parse_mode is None) else parse_mode + if data and not(video): + # function typo miss compatibility + video = data + + return types.Message.de_json( + await asyncio_helper.send_video( + self.token, chat_id, video, duration, caption, reply_to_message_id, reply_markup, + parse_mode, supports_streaming, disable_notification, timeout, thumb, width, height, + caption_entities, allow_sending_without_reply, protect_content)) + + async def send_animation( + self, chat_id: Union[int, str], animation: Union[Any, str], + duration: Optional[int]=None, + width: Optional[int]=None, + height: Optional[int]=None, + thumb: Optional[Union[Any, str]]=None, + caption: Optional[str]=None, + parse_mode: Optional[str]=None, + caption_entities: Optional[List[types.MessageEntity]]=None, + disable_notification: Optional[bool]=None, + protect_content: Optional[bool]=None, + reply_to_message_id: Optional[int]=None, + allow_sending_without_reply: Optional[bool]=None, + reply_markup: Optional[REPLY_MARKUP_TYPES]=None, + timeout: Optional[int]=None, ) -> types.Message: + """ + Use this method to send animation files (GIF or H.264/MPEG-4 AVC video without sound). + + Telegram documentation: https://core.telegram.org/bots/api#sendanimation + + :param chat_id: Integer : Unique identifier for the message recipient — User or GroupChat id + :param animation: InputFile or String : Animation to send. You can either pass a file_id as String to resend an + animation that is already on the Telegram server + :param duration: Integer : Duration of sent video in seconds + :param width: Integer : Video width + :param height: Integer : Video height + :param thumb: InputFile or String : Thumbnail of the file sent + :param caption: String : Animation caption (may also be used when resending animation by file_id). + :param parse_mode: + :param protect_content: + :param reply_to_message_id: + :param reply_markup: + :param disable_notification: + :param timeout: + :param caption_entities: + :param allow_sending_without_reply: + :return: + """ + parse_mode = self.parse_mode if (parse_mode is None) else parse_mode + + return types.Message.de_json( + await asyncio_helper.send_animation( + self.token, chat_id, animation, duration, caption, reply_to_message_id, + reply_markup, parse_mode, disable_notification, timeout, thumb, + caption_entities, allow_sending_without_reply, width, height, protect_content)) + + async def send_video_note( + self, chat_id: Union[int, str], data: Union[Any, str], + duration: Optional[int]=None, + length: Optional[int]=None, + reply_to_message_id: Optional[int]=None, + reply_markup: Optional[REPLY_MARKUP_TYPES]=None, + disable_notification: Optional[bool]=None, + timeout: Optional[int]=None, + thumb: Optional[Union[Any, str]]=None, + allow_sending_without_reply: Optional[bool]=None, + protect_content: Optional[bool]=None) -> types.Message: + """ + As of v.4.0, Telegram clients support rounded square mp4 videos of up to 1 minute long. Use this method to send + video messages. + + Telegram documentation: https://core.telegram.org/bots/api#sendvideonote + + :param chat_id: Integer : Unique identifier for the message recipient — User or GroupChat id + :param data: InputFile or String : Video note to send. You can either pass a file_id as String to resend + a video that is already on the Telegram server + :param duration: Integer : Duration of sent video in seconds + :param length: Integer : Video width and height, Can't be None and should be in range of (0, 640) + :param reply_to_message_id: + :param reply_markup: + :param disable_notification: + :param timeout: + :param thumb: InputFile or String : Thumbnail of the file sent + :param allow_sending_without_reply: + :param protect_content: + :return: + """ + return types.Message.de_json( + await asyncio_helper.send_video_note( + self.token, chat_id, data, duration, length, reply_to_message_id, reply_markup, + disable_notification, timeout, thumb, allow_sending_without_reply, protect_content)) + + async def send_media_group( + self, chat_id: Union[int, str], + media: List[Union[ + types.InputMediaAudio, types.InputMediaDocument, + types.InputMediaPhoto, types.InputMediaVideo]], + disable_notification: Optional[bool]=None, + protect_content: Optional[bool]=None, + reply_to_message_id: Optional[int]=None, + timeout: Optional[int]=None, + allow_sending_without_reply: Optional[bool]=None) -> List[types.Message]: + """ + send a group of photos or videos as an album. On success, an array of the sent Messages is returned. + + Telegram documentation: https://core.telegram.org/bots/api#sendmediagroup + + :param chat_id: + :param media: + :param disable_notification: + :param reply_to_message_id: + :param timeout: + :param allow_sending_without_reply: + :param protect_content: + :return: + """ + result = await asyncio_helper.send_media_group( + self.token, chat_id, media, disable_notification, reply_to_message_id, timeout, + allow_sending_without_reply, protect_content) + return [types.Message.de_json(msg) for msg in result] + + async def send_location( + self, chat_id: Union[int, str], + latitude: float, longitude: float, + live_period: Optional[int]=None, + reply_to_message_id: Optional[int]=None, + reply_markup: Optional[REPLY_MARKUP_TYPES]=None, + disable_notification: Optional[bool]=None, + timeout: Optional[int]=None, + horizontal_accuracy: Optional[float]=None, + heading: Optional[int]=None, + proximity_alert_radius: Optional[int]=None, + allow_sending_without_reply: Optional[bool]=None, + protect_content: Optional[bool]=None) -> types.Message: + + + """ + Use this method to send point on the map. + + Telegram documentation: https://core.telegram.org/bots/api#sendlocation + + :param chat_id: + :param latitude: + :param longitude: + :param live_period: + :param reply_to_message_id: + :param reply_markup: + :param disable_notification: + :param timeout: + :param horizontal_accuracy: + :param heading: + :param proximity_alert_radius: + :param allow_sending_without_reply: + :param protect_content: + :return: API reply. + """ + return types.Message.de_json( + await asyncio_helper.send_location( + self.token, chat_id, latitude, longitude, live_period, + reply_to_message_id, reply_markup, disable_notification, timeout, + horizontal_accuracy, heading, proximity_alert_radius, + allow_sending_without_reply, protect_content)) + + async def edit_message_live_location( + self, latitude: float, longitude: float, + chat_id: Optional[Union[int, str]]=None, + message_id: Optional[int]=None, + inline_message_id: Optional[str]=None, + reply_markup: Optional[REPLY_MARKUP_TYPES]=None, + timeout: Optional[int]=None, + horizontal_accuracy: Optional[float]=None, + heading: Optional[int]=None, + proximity_alert_radius: Optional[int]=None) -> types.Message: + """ + Use this method to edit live location. + + Telegram documentation: https://core.telegram.org/bots/api#editmessagelivelocation + + :param latitude: + :param longitude: + :param chat_id: + :param message_id: + :param reply_markup: + :param timeout: + :param inline_message_id: + :param horizontal_accuracy: + :param heading: + :param proximity_alert_radius: + :return: + """ + return types.Message.de_json( + await asyncio_helper.edit_message_live_location( + self.token, latitude, longitude, chat_id, message_id, + inline_message_id, reply_markup, timeout, + horizontal_accuracy, heading, proximity_alert_radius)) + + async def stop_message_live_location( + self, chat_id: Optional[Union[int, str]]=None, + message_id: Optional[int]=None, + inline_message_id: Optional[str]=None, + reply_markup: Optional[REPLY_MARKUP_TYPES]=None, + timeout: Optional[int]=None) -> types.Message: + """ + Use this method to stop updating a live location message sent by the bot + or via the bot (for inline bots) before live_period expires. + + Telegram documentation: https://core.telegram.org/bots/api#stopmessagelivelocation + + :param chat_id: + :param message_id: + :param inline_message_id: + :param reply_markup: + :param timeout: + :return: + """ + return types.Message.de_json( + await asyncio_helper.stop_message_live_location( + self.token, chat_id, message_id, inline_message_id, reply_markup, timeout)) + + async def send_venue( + self, chat_id: Union[int, str], + latitude: float, longitude: float, + title: str, address: str, + foursquare_id: Optional[str]=None, + foursquare_type: Optional[str]=None, + disable_notification: Optional[bool]=None, + reply_to_message_id: Optional[int]=None, + reply_markup: Optional[REPLY_MARKUP_TYPES]=None, + timeout: Optional[int]=None, + allow_sending_without_reply: Optional[bool]=None, + google_place_id: Optional[str]=None, + google_place_type: Optional[str]=None, + protect_content: Optional[bool]=None) -> types.Message: + """ + Use this method to send information about a venue. + + Telegram documentation: https://core.telegram.org/bots/api#sendvenue + + :param chat_id: Integer or String : Unique identifier for the target chat or username of the target channel + :param latitude: Float : Latitude of the venue + :param longitude: Float : Longitude of the venue + :param title: String : Name of the venue + :param address: String : Address of the venue + :param foursquare_id: String : Foursquare identifier of the venue + :param foursquare_type: Foursquare type of the venue, if known. (For example, “arts_entertainment/async default”, + “arts_entertainment/aquarium” or “food/icecream”.) + :param disable_notification: + :param reply_to_message_id: + :param reply_markup: + :param timeout: + :param allow_sending_without_reply: + :param google_place_id: + :param google_place_type: + :param protect_content: + :return: + """ + return types.Message.de_json( + await asyncio_helper.send_venue( + self.token, chat_id, latitude, longitude, title, address, foursquare_id, foursquare_type, + disable_notification, reply_to_message_id, reply_markup, timeout, + allow_sending_without_reply, google_place_id, google_place_type, protect_content) + ) + + async def send_contact( + self, chat_id: Union[int, str], phone_number: str, + first_name: str, last_name: Optional[str]=None, + vcard: Optional[str]=None, + disable_notification: Optional[bool]=None, + reply_to_message_id: Optional[int]=None, + reply_markup: Optional[REPLY_MARKUP_TYPES]=None, + timeout: Optional[int]=None, + allow_sending_without_reply: Optional[bool]=None, + protect_content: Optional[bool]=None) -> types.Message: + """ + Use this method to send phone contacts. + + Telegram documentation: https://core.telegram.org/bots/api#sendcontact + + :param chat_id: Integer or String : Unique identifier for the target chat or username of the target channel + :param phone_number: String : Contact's phone number + :param first_name: String : Contact's first name + :param last_name: String : Contact's last name + :param vcard: String : Additional data about the contact in the form of a vCard, 0-2048 bytes + :param disable_notification: + :param reply_to_message_id: + :param reply_markup: + :param timeout: + :param allow_sending_without_reply: + :param protect_content: + """ + return types.Message.de_json( + await asyncio_helper.send_contact( + self.token, chat_id, phone_number, first_name, last_name, vcard, + disable_notification, reply_to_message_id, reply_markup, timeout, + allow_sending_without_reply, protect_content) + ) + + async def send_chat_action( + self, chat_id: Union[int, str], action: str, timeout: Optional[int]=None) -> bool: + """ + Use this method when you need to tell the user that something is happening on the bot's side. + The status is set for 5 seconds or less (when a message arrives from your bot, Telegram clients clear + its typing status). + + Telegram documentation: https://core.telegram.org/bots/api#sendchataction + + :param chat_id: + :param action: One of the following strings: 'typing', 'upload_photo', 'record_video', 'upload_video', + 'record_audio', 'upload_audio', 'upload_document', 'find_location', 'record_video_note', + 'upload_video_note'. + :param timeout: + :return: API reply. :type: boolean + """ + return await asyncio_helper.send_chat_action(self.token, chat_id, action, timeout) + + async def kick_chat_member( + self, chat_id: Union[int, str], user_id: int, + until_date:Optional[Union[int, datetime]]=None, + revoke_messages: Optional[bool]=None) -> bool: + """ + This function is deprecated. Use `ban_chat_member` instead + """ + logger.info('kick_chat_member is deprecated. Use ban_chat_member instead.') + return await asyncio_helper.ban_chat_member(self.token, chat_id, user_id, until_date, revoke_messages) + + async def ban_chat_member( + self, chat_id: Union[int, str], user_id: int, + until_date:Optional[Union[int, datetime]]=None, + revoke_messages: Optional[bool]=None) -> bool: + """ + Use this method to ban a user in a group, a supergroup or a channel. + In the case of supergroups and channels, the user will not be able to return to the chat on their + own using invite links, etc., unless unbanned first. + Returns True on success. + + Telegram documentation: https://core.telegram.org/bots/api#banchatmember + + :param chat_id: Int or string : Unique identifier for the target group or username of the target supergroup + :param user_id: Int : Unique identifier of the target user + :param until_date: Date when the user will be unbanned, unix time. If user is banned for more than 366 days or + less than 30 seconds from the current time they are considered to be banned forever + :param revoke_messages: Bool: Pass True to delete all messages from the chat for the user that is being removed. + If False, the user will be able to see messages in the group that were sent before the user was removed. + Always True for supergroups and channels. + :return: boolean + """ + return await asyncio_helper.ban_chat_member(self.token, chat_id, user_id, until_date, revoke_messages) + + async def unban_chat_member( + self, chat_id: Union[int, str], user_id: int, + only_if_banned: Optional[bool]=False) -> bool: + """ + Use this method to unban a previously kicked user in a supergroup or channel. + The user will not return to the group or channel automatically, but will be able to join via link, etc. + The bot must be an administrator for this to work. By async default, this method guarantees that after the call + the user is not a member of the chat, but will be able to join it. So if the user is a member of the chat + they will also be removed from the chat. If you don't want this, use the parameter only_if_banned. + + Telegram documentation: https://core.telegram.org/bots/api#unbanchatmember + + :param chat_id: Unique identifier for the target group or username of the target supergroup or channel + (in the format @username) + :param user_id: Unique identifier of the target user + :param only_if_banned: Do nothing if the user is not banned + :return: True on success + """ + return await asyncio_helper.unban_chat_member(self.token, chat_id, user_id, only_if_banned) + + async def restrict_chat_member( + self, chat_id: Union[int, str], user_id: int, + until_date: Optional[Union[int, datetime]]=None, + can_send_messages: Optional[bool]=None, + can_send_media_messages: Optional[bool]=None, + can_send_polls: Optional[bool]=None, + can_send_other_messages: Optional[bool]=None, + can_add_web_page_previews: Optional[bool]=None, + can_change_info: Optional[bool]=None, + can_invite_users: Optional[bool]=None, + can_pin_messages: Optional[bool]=None) -> bool: + """ + Use this method to restrict a user in a supergroup. + The bot must be an administrator in the supergroup for this to work and must have + the appropriate admin rights. Pass True for all boolean parameters to lift restrictions from a user. + + Telegram documentation: https://core.telegram.org/bots/api#restrictchatmember + + :param chat_id: Int or String : Unique identifier for the target group or username of the target supergroup or channel (in the format @channelusername) + :param user_id: Int : Unique identifier of the target user + :param until_date: Date when restrictions will be lifted for the user, unix time. + If user is restricted for more than 366 days or less than 30 seconds from the current time, + they are considered to be restricted forever + :param can_send_messages: Pass True, if the user can send text messages, contacts, locations and venues + :param can_send_media_messages: Pass True, if the user can send audios, documents, photos, videos, video notes + and voice notes, implies can_send_messages + :param can_send_polls: Pass True, if the user is allowed to send polls, implies can_send_messages + :param can_send_other_messages: Pass True, if the user can send animations, games, stickers and + use inline bots, implies can_send_media_messages + :param can_add_web_page_previews: Pass True, if the user may add web page previews to their messages, implies can_send_media_messages + :param can_change_info: Pass True, if the user is allowed to change the chat title, photo and other settings. Ignored in public supergroups + :param can_invite_users: Pass True, if the user is allowed to invite new users to the chat, implies can_invite_users + :param can_pin_messages: Pass True, if the user is allowed to pin messages. Ignored in public supergroups + :return: True on success + """ + return await asyncio_helper.restrict_chat_member( + self.token, chat_id, user_id, until_date, + can_send_messages, can_send_media_messages, + can_send_polls, can_send_other_messages, + can_add_web_page_previews, can_change_info, + can_invite_users, can_pin_messages) + + async def promote_chat_member( + self, chat_id: Union[int, str], user_id: int, + can_change_info: Optional[bool]=None, + can_post_messages: Optional[bool]=None, + can_edit_messages: Optional[bool]=None, + can_delete_messages: Optional[bool]=None, + can_invite_users: Optional[bool]=None, + can_restrict_members: Optional[bool]=None, + can_pin_messages: Optional[bool]=None, + can_promote_members: Optional[bool]=None, + is_anonymous: Optional[bool]=None, + can_manage_chat: Optional[bool]=None, + can_manage_voice_chats: Optional[bool]=None) -> bool: + """ + Use this method to promote or demote a user in a supergroup or a channel. The bot must be an administrator + in the chat for this to work and must have the appropriate admin rights. + Pass False for all boolean parameters to demote a user. + + Telegram documentation: https://core.telegram.org/bots/api#promotechatmember + + :param chat_id: Unique identifier for the target chat or username of the target channel ( + in the format @channelusername) + :param user_id: Int : Unique identifier of the target user + :param can_change_info: Bool: Pass True, if the administrator can change chat title, photo and other settings + :param can_post_messages: Bool : Pass True, if the administrator can create channel posts, channels only + :param can_edit_messages: Bool : Pass True, if the administrator can edit messages of other users, channels only + :param can_delete_messages: Bool : Pass True, if the administrator can delete messages of other users + :param can_invite_users: Bool : Pass True, if the administrator can invite new users to the chat + :param can_restrict_members: Bool: Pass True, if the administrator can restrict, ban or unban chat members + :param can_pin_messages: Bool: Pass True, if the administrator can pin messages, supergroups only + :param can_promote_members: Bool: Pass True, if the administrator can add new administrators with a subset + of his own privileges or demote administrators that he has promoted, directly or indirectly + (promoted by administrators that were appointed by him) + :param is_anonymous: Bool: Pass True, if the administrator's presence in the chat is hidden + :param can_manage_chat: Bool: Pass True, if the administrator can access the chat event log, chat statistics, + message statistics in channels, see channel members, + see anonymous administrators in supergroups and ignore slow mode. + Implied by any other administrator privilege + :param can_manage_voice_chats: Bool: Pass True, if the administrator can manage voice chats + For now, bots can use this privilege only for passing to other administrators. + :return: True on success. + """ + return await asyncio_helper.promote_chat_member( + self.token, chat_id, user_id, can_change_info, can_post_messages, + can_edit_messages, can_delete_messages, can_invite_users, + can_restrict_members, can_pin_messages, can_promote_members, + is_anonymous, can_manage_chat, can_manage_voice_chats) + + async def set_chat_administrator_custom_title( + self, chat_id: Union[int, str], user_id: int, custom_title: str) -> bool: + """ + Use this method to set a custom title for an administrator + in a supergroup promoted by the bot. + + Telegram documentation: https://core.telegram.org/bots/api#setchatadministratorcustomtitle + + :param chat_id: Unique identifier for the target chat or username of the target supergroup + (in the format @supergroupusername) + :param user_id: Unique identifier of the target user + :param custom_title: New custom title for the administrator; + 0-16 characters, emoji are not allowed + :return: True on success. + """ + return await asyncio_helper.set_chat_administrator_custom_title(self.token, chat_id, user_id, custom_title) + + + async def ban_chat_sender_chat(self, chat_id: Union[int, str], sender_chat_id: Union[int, str]) -> bool: + """ + Use this method to ban a channel chat in a supergroup or a channel. + The owner of the chat will not be able to send messages and join live + streams on behalf of the chat, unless it is unbanned first. + The bot must be an administrator in the supergroup or channel + for this to work and must have the appropriate administrator rights. + Returns True on success. + + Telegram documentation: https://core.telegram.org/bots/api#banchatsenderchat + + :param chat_id: Unique identifier for the target chat or username of the target channel (in the format @channelusername) + :param sender_chat_id: Unique identifier of the target sender chat + :return: True on success. + """ + return await asyncio_helper.ban_chat_sender_chat(self.token, chat_id, sender_chat_id) + + async def unban_chat_sender_chat(self, chat_id: Union[int, str], sender_chat_id: Union[int, str]) -> bool: + """ + Use this method to unban a previously banned channel chat in a supergroup or channel. + The bot must be an administrator for this to work and must have the appropriate + administrator rights. + Returns True on success. + + Telegram documentation: https://core.telegram.org/bots/api#unbanchatsenderchat + + :params: + :param chat_id: Unique identifier for the target chat or username of the target channel (in the format @channelusername) + :param sender_chat_id: Unique identifier of the target sender chat + :return: True on success. + """ + return await asyncio_helper.unban_chat_sender_chat(self.token, chat_id, sender_chat_id) + + async def set_chat_permissions( + self, chat_id: Union[int, str], permissions: types.ChatPermissions) -> bool: + """ + Use this method to set async default chat permissions for all members. + The bot must be an administrator in the group or a supergroup for this to work + and must have the can_restrict_members admin rights. + + Telegram documentation: https://core.telegram.org/bots/api#setchatpermissions + + :param chat_id: Unique identifier for the target chat or username of the target supergroup + (in the format @supergroupusername) + :param permissions: New async default chat permissions + :return: True on success + """ + return await asyncio_helper.set_chat_permissions(self.token, chat_id, permissions) + + async def create_chat_invite_link( + self, chat_id: Union[int, str], + name: Optional[str]=None, + expire_date: Optional[Union[int, datetime]]=None, + member_limit: Optional[int]=None, + creates_join_request: Optional[bool]=None) -> types.ChatInviteLink: + """ + Use this method to create an additional invite link for a chat. + The bot must be an administrator in the chat for this to work and must have the appropriate admin rights. + + Telegram documentation: https://core.telegram.org/bots/api#createchatinvitelink + + :param chat_id: Id: Unique identifier for the target chat or username of the target channel + (in the format @channelusername) + :param name: Invite link name; 0-32 characters + :param expire_date: Point in time (Unix timestamp) when the link will expire + :param member_limit: Maximum number of users that can be members of the chat simultaneously + :param creates_join_request: True, if users joining the chat via the link need to be approved by chat administrators. If True, member_limit can't be specified + :return: + """ + return types.ChatInviteLink.de_json( + await asyncio_helper.create_chat_invite_link(self.token, chat_id, name, expire_date, member_limit, creates_join_request) + ) + + async def edit_chat_invite_link( + self, chat_id: Union[int, str], + invite_link: Optional[str] = None, + name: Optional[str]=None, + expire_date: Optional[Union[int, datetime]]=None, + member_limit: Optional[int]=None, + creates_join_request: Optional[bool]=None) -> types.ChatInviteLink: + """ + Use this method to edit a non-primary invite link created by the bot. + The bot must be an administrator in the chat for this to work and must have the appropriate admin rights. + + Telegram documentation: https://core.telegram.org/bots/api#editchatinvitelink + + :param chat_id: Id: Unique identifier for the target chat or username of the target channel + (in the format @channelusername) + :param name: Invite link name; 0-32 characters + :param invite_link: The invite link to edit + :param expire_date: Point in time (Unix timestamp) when the link will expire + :param member_limit: Maximum number of users that can be members of the chat simultaneously + :param creates_join_request: True, if users joining the chat via the link need to be approved by chat administrators. If True, member_limit can't be specified + :return: + """ + return types.ChatInviteLink.de_json( + await asyncio_helper.edit_chat_invite_link(self.token, chat_id, name, invite_link, expire_date, member_limit, creates_join_request) + ) + + async def revoke_chat_invite_link( + self, chat_id: Union[int, str], invite_link: str) -> types.ChatInviteLink: + """ + Use this method to revoke an invite link created by the bot. + Note: If the primary link is revoked, a new link is automatically generated The bot must be an administrator + in the chat for this to work and must have the appropriate admin rights. + + Telegram documentation: https://core.telegram.org/bots/api#revokechatinvitelink + + :param chat_id: Id: Unique identifier for the target chat or username of the target channel(in the format @channelusername) + :param invite_link: The invite link to revoke + :return: API reply. + """ + return types.ChatInviteLink.de_json( + await asyncio_helper.revoke_chat_invite_link(self.token, chat_id, invite_link) + ) + + async def export_chat_invite_link(self, chat_id: Union[int, str]) -> str: + """ + Use this method to export an invite link to a supergroup or a channel. The bot must be an administrator + in the chat for this to work and must have the appropriate admin rights. + + Telegram documentation: https://core.telegram.org/bots/api#exportchatinvitelink + + :param chat_id: Id: Unique identifier for the target chat or username of the target channel + (in the format @channelusername) + :return: exported invite link as String on success. + """ + return await asyncio_helper.export_chat_invite_link(self.token, chat_id) + + + async def approve_chat_join_request(self, chat_id: Union[str, int], user_id: Union[int, str]) -> bool: + """ + Use this method to approve a chat join request. + The bot must be an administrator in the chat for this to work and must have + the can_invite_users administrator right. Returns True on success. + + Telegram documentation: https://core.telegram.org/bots/api#approvechatjoinrequest + + :param chat_id: Unique identifier for the target chat or username of the target supergroup + (in the format @supergroupusername) + :param user_id: Unique identifier of the target user + :return: True on success. + """ + return await asyncio_helper.approve_chat_join_request(self.token, chat_id, user_id) + + async def decline_chat_join_request(self, chat_id: Union[str, int], user_id: Union[int, str]) -> bool: + """ + Use this method to decline a chat join request. + The bot must be an administrator in the chat for this to work and must have + the can_invite_users administrator right. Returns True on success. + + Telegram documentation: https://core.telegram.org/bots/api#declinechatjoinrequest + + :param chat_id: Unique identifier for the target chat or username of the target supergroup + (in the format @supergroupusername) + :param user_id: Unique identifier of the target user + :return: True on success. + """ + return await asyncio_helper.decline_chat_join_request(self.token, chat_id, user_id) + + async def set_chat_photo(self, chat_id: Union[int, str], photo: Any) -> bool: + """ + Use this method to set a new profile photo for the chat. Photos can't be changed for private chats. + The bot must be an administrator in the chat for this to work and must have the appropriate admin rights. + Returns True on success. + Note: In regular groups (non-supergroups), this method will only work if the ‘All Members Are Admins’ setting is off in the target group. + + Telegram documentation: https://core.telegram.org/bots/api#setchatphoto + + :param chat_id: Int or Str: Unique identifier for the target chat or username of the target channel (in the format @channelusername) + :param photo: InputFile: New chat photo, uploaded using multipart/form-data + :return: + """ + return await asyncio_helper.set_chat_photo(self.token, chat_id, photo) + + async def delete_chat_photo(self, chat_id: Union[int, str]) -> bool: + """ + Use this method to delete a chat photo. Photos can't be changed for private chats. + The bot must be an administrator in the chat for this to work and must have the appropriate admin rights. + Returns True on success. + Note: In regular groups (non-supergroups), this method will only work if the ‘All Members Are Admins’ + setting is off in the target group. + + Telegram documentation: https://core.telegram.org/bots/api#deletechatphoto + + :param chat_id: Int or Str: Unique identifier for the target chat or username of the target channel + (in the format @channelusername) + """ + return await asyncio_helper.delete_chat_photo(self.token, chat_id) + + async def get_my_commands(self, scope: Optional[types.BotCommandScope], + language_code: Optional[str]) -> List[types.BotCommand]: + """ + Use this method to get the current list of the bot's commands. + Returns List of BotCommand on success. + + Telegram documentation: https://core.telegram.org/bots/api#getmycommands + + :param scope: The scope of users for which the commands are relevant. + async defaults to BotCommandScopeasync default. + :param language_code: A two-letter ISO 639-1 language code. If empty, + commands will be applied to all users from the given scope, + for whose language there are no dedicated commands + """ + result = await asyncio_helper.get_my_commands(self.token, scope, language_code) + return [types.BotCommand.de_json(cmd) for cmd in result] + + async def set_my_commands(self, commands: List[types.BotCommand], + scope: Optional[types.BotCommandScope]=None, + language_code: Optional[str]=None) -> bool: + """ + Use this method to change the list of the bot's commands. + + Telegram documentation: https://core.telegram.org/bots/api#setmycommands + + :param commands: List of BotCommand. At most 100 commands can be specified. + :param scope: The scope of users for which the commands are relevant. + async defaults to BotCommandScopeasync default. + :param language_code: A two-letter ISO 639-1 language code. If empty, + commands will be applied to all users from the given scope, + for whose language there are no dedicated commands + :return: + """ + return await asyncio_helper.set_my_commands(self.token, commands, scope, language_code) + + async def delete_my_commands(self, scope: Optional[types.BotCommandScope]=None, + language_code: Optional[int]=None) -> bool: + """ + Use this method to delete the list of the bot's commands for the given scope and user language. + After deletion, higher level commands will be shown to affected users. + Returns True on success. + + Telegram documentation: https://core.telegram.org/bots/api#deletemycommands + + :param scope: The scope of users for which the commands are relevant. + async defaults to BotCommandScopeasync default. + :param language_code: A two-letter ISO 639-1 language code. If empty, + commands will be applied to all users from the given scope, + for whose language there are no dedicated commands + """ + return await asyncio_helper.delete_my_commands(self.token, scope, language_code) + + async def set_chat_title(self, chat_id: Union[int, str], title: str) -> bool: + """ + Use this method to change the title of a chat. Titles can't be changed for private chats. + The bot must be an administrator in the chat for this to work and must have the appropriate admin rights. + Returns True on success. + Note: In regular groups (non-supergroups), this method will only work if the ‘All Members Are Admins’ + setting is off in the target group. + + Telegram documentation: https://core.telegram.org/bots/api#setchattitle + + :param chat_id: Int or Str: Unique identifier for the target chat or username of the target channel + (in the format @channelusername) + :param title: New chat title, 1-255 characters + :return: + """ + return await asyncio_helper.set_chat_title(self.token, chat_id, title) + + async def set_chat_description(self, chat_id: Union[int, str], description: Optional[str]=None) -> bool: + """ + Use this method to change the description of a supergroup or a channel. + The bot must be an administrator in the chat for this to work and must have the appropriate admin rights. + + Telegram documentation: https://core.telegram.org/bots/api#setchatdescription + + :param chat_id: Int or Str: Unique identifier for the target chat or username of the target channel + (in the format @channelusername) + :param description: Str: New chat description, 0-255 characters + :return: True on success. + """ + return await asyncio_helper.set_chat_description(self.token, chat_id, description) + + async def pin_chat_message( + self, chat_id: Union[int, str], message_id: int, + disable_notification: Optional[bool]=False) -> bool: + """ + Use this method to pin a message in a supergroup. + The bot must be an administrator in the chat for this to work and must have the appropriate admin rights. + Returns True on success. + + Telegram documentation: https://core.telegram.org/bots/api#pinchatmessage + + :param chat_id: Int or Str: Unique identifier for the target chat or username of the target channel + (in the format @channelusername) + :param message_id: Int: Identifier of a message to pin + :param disable_notification: Bool: Pass True, if it is not necessary to send a notification + to all group members about the new pinned message + :return: + """ + return await asyncio_helper.pin_chat_message(self.token, chat_id, message_id, disable_notification) + + async def unpin_chat_message(self, chat_id: Union[int, str], message_id: Optional[int]=None) -> bool: + """ + Use this method to unpin specific pinned message in a supergroup chat. + The bot must be an administrator in the chat for this to work and must have the appropriate admin rights. + Returns True on success. + + Telegram documentation: https://core.telegram.org/bots/api#unpinchatmessage + + :param chat_id: Int or Str: Unique identifier for the target chat or username of the target channel + (in the format @channelusername) + :param message_id: Int: Identifier of a message to unpin + :return: + """ + return await asyncio_helper.unpin_chat_message(self.token, chat_id, message_id) + + async def unpin_all_chat_messages(self, chat_id: Union[int, str]) -> bool: + """ + Use this method to unpin a all pinned messages in a supergroup chat. + The bot must be an administrator in the chat for this to work and must have the appropriate admin rights. + Returns True on success. + + Telegram documentation: https://core.telegram.org/bots/api#unpinallchatmessages + + :param chat_id: Int or Str: Unique identifier for the target chat or username of the target channel + (in the format @channelusername) + :return: + """ + return await asyncio_helper.unpin_all_chat_messages(self.token, chat_id) + + async def edit_message_text( + self, text: str, + chat_id: Optional[Union[int, str]]=None, + message_id: Optional[int]=None, + inline_message_id: Optional[str]=None, + parse_mode: Optional[str]=None, + entities: Optional[List[types.MessageEntity]]=None, + disable_web_page_preview: Optional[bool]=None, + reply_markup: Optional[REPLY_MARKUP_TYPES]=None) -> Union[types.Message, bool]: + """ + Use this method to edit text and game messages. + + Telegram documentation: https://core.telegram.org/bots/api#editmessagetext + + :param text: + :param chat_id: + :param message_id: + :param inline_message_id: + :param parse_mode: + :param entities: + :param disable_web_page_preview: + :param reply_markup: + :return: + """ + parse_mode = self.parse_mode if (parse_mode is None) else parse_mode + + result = await asyncio_helper.edit_message_text(self.token, text, chat_id, message_id, inline_message_id, parse_mode, + entities, disable_web_page_preview, reply_markup) + if type(result) == bool: # if edit inline message return is bool not Message. + return result + return types.Message.de_json(result) + + async def edit_message_media( + self, media: Any, chat_id: Optional[Union[int, str]]=None, + message_id: Optional[int]=None, + inline_message_id: Optional[str]=None, + reply_markup: Optional[REPLY_MARKUP_TYPES]=None) -> Union[types.Message, bool]: + """ + Use this method to edit animation, audio, document, photo, or video messages. + If a message is a part of a message album, then it can be edited only to a photo or a video. + Otherwise, message type can be changed arbitrarily. When inline message is edited, new file can't be uploaded. + Use previously uploaded file via its file_id or specify a URL. + + Telegram documentation: https://core.telegram.org/bots/api#editmessagemedia + + :param media: + :param chat_id: + :param message_id: + :param inline_message_id: + :param reply_markup: + :return: + """ + result = await asyncio_helper.edit_message_media(self.token, media, chat_id, message_id, inline_message_id, reply_markup) + if type(result) == bool: # if edit inline message return is bool not Message. + return result + return types.Message.de_json(result) + + async def edit_message_reply_markup( + self, chat_id: Optional[Union[int, str]]=None, + message_id: Optional[int]=None, + inline_message_id: Optional[str]=None, + reply_markup: Optional[REPLY_MARKUP_TYPES]=None) -> Union[types.Message, bool]: + """ + Use this method to edit only the reply markup of messages. + + Telegram documentation: https://core.telegram.org/bots/api#editmessagereplymarkup + + :param chat_id: + :param message_id: + :param inline_message_id: + :param reply_markup: + :return: + """ + result = await asyncio_helper.edit_message_reply_markup(self.token, chat_id, message_id, inline_message_id, reply_markup) + if type(result) == bool: + return result + return types.Message.de_json(result) + + async def send_game( + self, chat_id: Union[int, str], game_short_name: str, + disable_notification: Optional[bool]=None, + reply_to_message_id: Optional[int]=None, + reply_markup: Optional[REPLY_MARKUP_TYPES]=None, + timeout: Optional[int]=None, + allow_sending_without_reply: Optional[bool]=None, + protect_content: Optional[bool]=None) -> types.Message: + """ + Used to send the game. + + Telegram documentation: https://core.telegram.org/bots/api#sendgame + + :param chat_id: + :param game_short_name: + :param disable_notification: + :param reply_to_message_id: + :param reply_markup: + :param timeout: + :param allow_sending_without_reply: + :param protect_content: + :return: + """ + result = await asyncio_helper.send_game( + self.token, chat_id, game_short_name, disable_notification, + reply_to_message_id, reply_markup, timeout, + allow_sending_without_reply, protect_content) + return types.Message.de_json(result) + + async def set_game_score( + self, user_id: Union[int, str], score: int, + force: Optional[bool]=None, + chat_id: Optional[Union[int, str]]=None, + message_id: Optional[int]=None, + inline_message_id: Optional[str]=None, + disable_edit_message: Optional[bool]=None) -> Union[types.Message, bool]: + """ + Sets the value of points in the game to a specific user. + + Telegram documentation: https://core.telegram.org/bots/api#setgamescore + + :param user_id: + :param score: + :param force: + :param chat_id: + :param message_id: + :param inline_message_id: + :param disable_edit_message: + :return: + """ + result = await asyncio_helper.set_game_score(self.token, user_id, score, force, disable_edit_message, chat_id, + message_id, inline_message_id) + if type(result) == bool: + return result + return types.Message.de_json(result) + + async def get_game_high_scores( + self, user_id: int, chat_id: Optional[Union[int, str]]=None, + message_id: Optional[int]=None, + inline_message_id: Optional[str]=None) -> List[types.GameHighScore]: + """ + Gets top points and game play. + + Telegram documentation: https://core.telegram.org/bots/api#getgamehighscores + + :param user_id: + :param chat_id: + :param message_id: + :param inline_message_id: + :return: + """ + result = await asyncio_helper.get_game_high_scores(self.token, user_id, chat_id, message_id, inline_message_id) + return [types.GameHighScore.de_json(r) for r in result] + + async def send_invoice( + self, chat_id: Union[int, str], title: str, description: str, + invoice_payload: str, provider_token: str, currency: str, + prices: List[types.LabeledPrice], start_parameter: Optional[str]=None, + photo_url: Optional[str]=None, photo_size: Optional[int]=None, + photo_width: Optional[int]=None, photo_height: Optional[int]=None, + need_name: Optional[bool]=None, need_phone_number: Optional[bool]=None, + need_email: Optional[bool]=None, need_shipping_address: Optional[bool]=None, + send_phone_number_to_provider: Optional[bool]=None, + send_email_to_provider: Optional[bool]=None, + is_flexible: Optional[bool]=None, + disable_notification: Optional[bool]=None, + reply_to_message_id: Optional[int]=None, + reply_markup: Optional[REPLY_MARKUP_TYPES]=None, + provider_data: Optional[str]=None, + timeout: Optional[int]=None, + allow_sending_without_reply: Optional[bool]=None, + max_tip_amount: Optional[int] = None, + suggested_tip_amounts: Optional[List[int]]=None, + protect_content: Optional[bool]=None) -> types.Message: + """ + Sends invoice. + + Telegram documentation: https://core.telegram.org/bots/api#sendinvoice + + :param chat_id: Unique identifier for the target private chat + :param title: Product name + :param description: Product description + :param invoice_payload: Bot-async defined invoice payload, 1-128 bytes. This will not be displayed to the user, + use for your internal processes. + :param provider_token: Payments provider token, obtained via @Botfather + :param currency: Three-letter ISO 4217 currency code, + see https://core.telegram.org/bots/payments#supported-currencies + :param prices: Price breakdown, a list of components + (e.g. product price, tax, discount, delivery cost, delivery tax, bonus, etc.) + :param start_parameter: Unique deep-linking parameter that can be used to generate this invoice + when used as a start parameter + :param photo_url: URL of the product photo for the invoice. Can be a photo of the goods + or a marketing image for a service. People like it better when they see what they are paying for. + :param photo_size: Photo size + :param photo_width: Photo width + :param photo_height: Photo height + :param need_name: Pass True, if you require the user's full name to complete the order + :param need_phone_number: Pass True, if you require the user's phone number to complete the order + :param need_email: Pass True, if you require the user's email to complete the order + :param need_shipping_address: Pass True, if you require the user's shipping address to complete the order + :param is_flexible: Pass True, if the final price depends on the shipping method + :param send_phone_number_to_provider: Pass True, if user's phone number should be sent to provider + :param send_email_to_provider: Pass True, if user's email address should be sent to provider + :param disable_notification: Sends the message silently. Users will receive a notification with no sound. + :param reply_to_message_id: If the message is a reply, ID of the original message + :param reply_markup: A JSON-serialized object for an inline keyboard. If empty, + one 'Pay total price' button will be shown. If not empty, the first button must be a Pay button + :param provider_data: A JSON-serialized data about the invoice, which will be shared with the payment provider. + A detailed description of required fields should be provided by the payment provider. + :param timeout: + :param allow_sending_without_reply: + :param max_tip_amount: The maximum accepted amount for tips in the smallest units of the currency + :param suggested_tip_amounts: A JSON-serialized array of suggested amounts of tips in the smallest + units of the currency. At most 4 suggested tip amounts can be specified. The suggested tip + amounts must be positive, passed in a strictly increased order and must not exceed max_tip_amount. + :param protect_content: + :return: + """ + result = await asyncio_helper.send_invoice( + self.token, chat_id, title, description, invoice_payload, provider_token, + currency, prices, start_parameter, photo_url, photo_size, photo_width, + photo_height, need_name, need_phone_number, need_email, need_shipping_address, + send_phone_number_to_provider, send_email_to_provider, is_flexible, disable_notification, + reply_to_message_id, reply_markup, provider_data, timeout, allow_sending_without_reply, + max_tip_amount, suggested_tip_amounts, protect_content) + return types.Message.de_json(result) + + # noinspection PyShadowingBuiltins + async def send_poll( + self, chat_id: Union[int, str], question: str, options: List[str], + is_anonymous: Optional[bool]=None, type: Optional[str]=None, + allows_multiple_answers: Optional[bool]=None, + correct_option_id: Optional[int]=None, + explanation: Optional[str]=None, + explanation_parse_mode: Optional[str]=None, + open_period: Optional[int]=None, + close_date: Optional[Union[int, datetime]]=None, + is_closed: Optional[bool]=None, + disable_notification: Optional[bool]=False, + reply_to_message_id: Optional[int]=None, + reply_markup: Optional[REPLY_MARKUP_TYPES]=None, + allow_sending_without_reply: Optional[bool]=None, + timeout: Optional[int]=None, + explanation_entities: Optional[List[types.MessageEntity]]=None, + protect_content: Optional[bool]=None) -> types.Message: + """ + Send polls. + + Telegram documentation: https://core.telegram.org/bots/api#sendpoll + + :param chat_id: + :param question: + :param options: array of str with answers + :param is_anonymous: + :param type: + :param allows_multiple_answers: + :param correct_option_id: + :param explanation: + :param explanation_parse_mode: + :param open_period: + :param close_date: + :param is_closed: + :param disable_notification: + :param reply_to_message_id: + :param allow_sending_without_reply: + :param reply_markup: + :param timeout: + :param explanation_entities: + :param protect_content: + :return: + """ + + if isinstance(question, types.Poll): + raise RuntimeError("The send_poll signature was changed, please see send_poll function details.") + + return types.Message.de_json( + await asyncio_helper.send_poll( + self.token, chat_id, + question, options, + is_anonymous, type, allows_multiple_answers, correct_option_id, + explanation, explanation_parse_mode, open_period, close_date, is_closed, + disable_notification, reply_to_message_id, allow_sending_without_reply, + reply_markup, timeout, explanation_entities, protect_content)) + + async def stop_poll( + self, chat_id: Union[int, str], message_id: int, + reply_markup: Optional[REPLY_MARKUP_TYPES]=None) -> types.Poll: + """ + Stops poll. + + Telegram documentation: https://core.telegram.org/bots/api#stoppoll + + :param chat_id: + :param message_id: + :param reply_markup: + :return: + """ + return types.Poll.de_json(await asyncio_helper.stop_poll(self.token, chat_id, message_id, reply_markup)) + + async def answer_shipping_query( + self, shipping_query_id: str, ok: bool, + shipping_options: Optional[List[types.ShippingOption]]=None, + error_message: Optional[str]=None) -> bool: + """ + Asks for an answer to a shipping question. + + Telegram documentation: https://core.telegram.org/bots/api#answershippingquery + + :param shipping_query_id: + :param ok: + :param shipping_options: + :param error_message: + :return: + """ + return await asyncio_helper.answer_shipping_query(self.token, shipping_query_id, ok, shipping_options, error_message) + + async def answer_pre_checkout_query( + self, pre_checkout_query_id: int, ok: bool, + error_message: Optional[str]=None) -> bool: + """ + Response to a request for pre-inspection. + + Telegram documentation: https://core.telegram.org/bots/api#answerprecheckoutquery + + :param pre_checkout_query_id: + :param ok: + :param error_message: + :return: + """ + return await asyncio_helper.answer_pre_checkout_query(self.token, pre_checkout_query_id, ok, error_message) + + async def edit_message_caption( + self, caption: str, chat_id: Optional[Union[int, str]]=None, + message_id: Optional[int]=None, + inline_message_id: Optional[str]=None, + parse_mode: Optional[str]=None, + caption_entities: Optional[List[types.MessageEntity]]=None, + reply_markup: Optional[REPLY_MARKUP_TYPES]=None) -> Union[types.Message, bool]: + """ + Use this method to edit captions of messages. + + Telegram documentation: https://core.telegram.org/bots/api#editmessagecaption + + :param caption: + :param chat_id: + :param message_id: + :param inline_message_id: + :param parse_mode: + :param caption_entities: + :param reply_markup: + :return: + """ + parse_mode = self.parse_mode if (parse_mode is None) else parse_mode + + result = await asyncio_helper.edit_message_caption(self.token, caption, chat_id, message_id, inline_message_id, + parse_mode, caption_entities, reply_markup) + if type(result) == bool: + return result + return types.Message.de_json(result) + + async def reply_to(self, message: types.Message, text: str, **kwargs) -> types.Message: + """ + Convenience function for `send_message(message.chat.id, text, reply_to_message_id=message.message_id, **kwargs)` + + :param message: + :param text: + :param kwargs: + :return: + """ + return await self.send_message(message.chat.id, text, reply_to_message_id=message.message_id, **kwargs) + + async def answer_inline_query( + self, inline_query_id: str, + results: List[Any], + cache_time: Optional[int]=None, + is_personal: Optional[bool]=None, + next_offset: Optional[str]=None, + switch_pm_text: Optional[str]=None, + switch_pm_parameter: Optional[str]=None) -> bool: + """ + Use this method to send answers to an inline query. On success, True is returned. + No more than 50 results per query are allowed. + + Telegram documentation: https://core.telegram.org/bots/api#answerinlinequery + + :param inline_query_id: Unique identifier for the answered query + :param results: Array of results for the inline query + :param cache_time: The maximum amount of time in seconds that the result of the inline query + may be cached on the server. + :param is_personal: Pass True, if results may be cached on the server side only for + the user that sent the query. + :param next_offset: Pass the offset that a client should send in the next query with the same text + to receive more results. + :param switch_pm_parameter: If passed, clients will display a button with specified text that switches the user + to a private chat with the bot and sends the bot a start message with the parameter switch_pm_parameter + :param switch_pm_text: Parameter for the start message sent to the bot when user presses the switch button + :return: True means success. + """ + return await asyncio_helper.answer_inline_query(self.token, inline_query_id, results, cache_time, is_personal, next_offset, + switch_pm_text, switch_pm_parameter) + + async def answer_callback_query( + self, callback_query_id: int, + text: Optional[str]=None, show_alert: Optional[bool]=None, + url: Optional[str]=None, cache_time: Optional[int]=None) -> bool: + """ + Use this method to send answers to callback queries sent from inline keyboards. The answer will be displayed to + the user as a notification at the top of the chat screen or as an alert. + + Telegram documentation: https://core.telegram.org/bots/api#answercallbackquery + + :param callback_query_id: + :param text: + :param show_alert: + :param url: + :param cache_time: + :return: + """ + return await asyncio_helper.answer_callback_query(self.token, callback_query_id, text, show_alert, url, cache_time) + + async def set_sticker_set_thumb( + self, name: str, user_id: int, thumb: Union[Any, str]=None): + """ + Use this method to set the thumbnail of a sticker set. + Animated thumbnails can be set for animated sticker sets only. Returns True on success. + + Telegram documentation: https://core.telegram.org/bots/api#setstickersetthumb + + :param name: Sticker set name + :param user_id: User identifier + :param thumb: A PNG image with the thumbnail, must be up to 128 kilobytes in size and have width and height + exactly 100px, or a TGS animation with the thumbnail up to 32 kilobytes in size; + see https://core.telegram.org/animated_stickers#technical-requirements + + """ + return await asyncio_helper.set_sticker_set_thumb(self.token, name, user_id, thumb) + + async def get_sticker_set(self, name: str) -> types.StickerSet: + """ + Use this method to get a sticker set. On success, a StickerSet object is returned. + + Telegram documentation: https://core.telegram.org/bots/api#getstickerset + + :param name: + :return: + """ + result = await asyncio_helper.get_sticker_set(self.token, name) + return types.StickerSet.de_json(result) + + async def upload_sticker_file(self, user_id: int, png_sticker: Union[Any, str]) -> types.File: + """ + Use this method to upload a .png file with a sticker for later use in createNewStickerSet and addStickerToSet + methods (can be used multiple times). Returns the uploaded File on success. + + + Telegram documentation: https://core.telegram.org/bots/api#uploadstickerfile + + :param user_id: + :param png_sticker: + :return: + """ + result = await asyncio_helper.upload_sticker_file(self.token, user_id, png_sticker) + return types.File.de_json(result) + + async def create_new_sticker_set( + self, user_id: int, name: str, title: str, + emojis: str, + png_sticker: Union[Any, str]=None, + tgs_sticker: Union[Any, str]=None, + webm_sticker: Union[Any, str]=None, + contains_masks: Optional[bool]=None, + mask_position: Optional[types.MaskPosition]=None) -> bool: + """ + Use this method to create new sticker set owned by a user. + The bot will be able to edit the created sticker set. + Returns True on success. + + Telegram documentation: https://core.telegram.org/bots/api#createnewstickerset + + :param user_id: + :param name: + :param title: + :param emojis: + :param png_sticker: + :param tgs_sticker: + :webm_sticker: + :param contains_masks: + :param mask_position: + :return: + """ + return await asyncio_helper.create_new_sticker_set( + self.token, user_id, name, title, emojis, png_sticker, tgs_sticker, + contains_masks, mask_position, webm_sticker) + + + async def add_sticker_to_set( + self, user_id: int, name: str, emojis: str, + png_sticker: Optional[Union[Any, str]]=None, + tgs_sticker: Optional[Union[Any, str]]=None, + webm_sticker: Optional[Union[Any, str]]=None, + mask_position: Optional[types.MaskPosition]=None) -> bool: + """ + Use this method to add a new sticker to a set created by the bot. + It's required to pass `png_sticker` or `tgs_sticker`. + Returns True on success. + + Telegram documentation: https://core.telegram.org/bots/api#addstickertoset + + :param user_id: + :param name: + :param emojis: + :param png_sticker: Required if `tgs_sticker` is None + :param tgs_sticker: Required if `png_sticker` is None + :webm_sticker: + :param mask_position: + :return: + """ + return await asyncio_helper.add_sticker_to_set( + self.token, user_id, name, emojis, png_sticker, tgs_sticker, mask_position, webm_sticker) + + + async def set_sticker_position_in_set(self, sticker: str, position: int) -> bool: + """ + Use this method to move a sticker in a set created by the bot to a specific position . Returns True on success. + + Telegram documentation: https://core.telegram.org/bots/api#setstickerpositioninset + + :param sticker: + :param position: + :return: + """ + return await asyncio_helper.set_sticker_position_in_set(self.token, sticker, position) + + async def delete_sticker_from_set(self, sticker: str) -> bool: + """ + Use this method to delete a sticker from a set created by the bot. Returns True on success. + + Telegram documentation: https://core.telegram.org/bots/api#deletestickerfromset + + :param sticker: + :return: + """ + return await asyncio_helper.delete_sticker_from_set(self.token, sticker) + + + async def set_state(self, user_id: int, state: str, chat_id: int=None): + """ + Sets a new state of a user. + + :param user_id: + :param chat_id: + :param state: new state. can be string or integer. + """ + if not chat_id: + chat_id = user_id + await self.current_states.set_state(chat_id, user_id, state) + + async def reset_data(self, user_id: int, chat_id: int=None): + """ + Reset data for a user in chat. + + :param user_id: + :param chat_id: + """ + if chat_id is None: + chat_id = user_id + await self.current_states.reset_data(chat_id, user_id) + + async def delete_state(self, user_id: int, chat_id:int=None): + """ + Delete the current state of a user. + + :param user_id: + :param chat_id: + :return: + """ + if not chat_id: + chat_id = user_id + await self.current_states.delete_state(chat_id, user_id) + + def retrieve_data(self, user_id: int, chat_id: int=None): + if not chat_id: + chat_id = user_id + return self.current_states.get_interactive_data(chat_id, user_id) + + async def get_state(self, user_id, chat_id: int=None): + """ + Get current state of a user. + + :param user_id: + :param chat_id: + :return: state of a user + """ + if not chat_id: + chat_id = user_id + return await self.current_states.get_state(chat_id, user_id) + + async def add_data(self, user_id: int, chat_id: int=None, **kwargs): + """ + Add data to states. + + :param user_id: + :param chat_id: + """ + if not chat_id: + chat_id = user_id + for key, value in kwargs.items(): + await self.current_states.set_data(chat_id, user_id, key, value) diff --git a/telebot/asyncio_filters.py b/telebot/asyncio_filters.py new file mode 100644 index 0000000..1b39761 --- /dev/null +++ b/telebot/asyncio_filters.py @@ -0,0 +1,328 @@ +from abc import ABC +from typing import Optional, Union +from telebot.asyncio_handler_backends import State + +from telebot import types + + +class SimpleCustomFilter(ABC): + """ + Simple Custom Filter base class. + Create child class with check() method. + Accepts only message, returns bool value, that is compared with given in handler. + """ + + async def check(self, message): + """ + Perform a check. + """ + pass + + +class AdvancedCustomFilter(ABC): + """ + Simple Custom Filter base class. + Create child class with check() method. + Accepts two parameters, returns bool: True - filter passed, False - filter failed. + message: Message class + text: Filter value given in handler + """ + + async def check(self, message, text): + """ + Perform a check. + """ + pass + + +class TextFilter: + """ + Advanced text filter to check (types.Message, types.CallbackQuery, types.InlineQuery, types.Poll) + + example of usage is in examples/custom_filters/advanced_text_filter.py + """ + + def __init__(self, + equals: Optional[str] = None, + contains: Optional[Union[list, tuple]] = None, + starts_with: Optional[Union[str, list, tuple]] = None, + ends_with: Optional[Union[str, list, tuple]] = None, + ignore_case: bool = False): + + """ + :param equals: string, True if object's text is equal to passed string + :param contains: list[str] or tuple[str], True if any string element of iterable is in text + :param starts_with: string, True if object's text starts with passed string + :param ends_with: string, True if object's text starts with passed string + :param ignore_case: bool (default False), case insensitive + """ + + to_check = sum((pattern is not None for pattern in (equals, contains, starts_with, ends_with))) + if to_check == 0: + raise ValueError('None of the check modes was specified') + + self.equals = equals + self.contains = self._check_iterable(contains, filter_name='contains') + self.starts_with = self._check_iterable(starts_with, filter_name='starts_with') + self.ends_with = self._check_iterable(ends_with, filter_name='ends_with') + self.ignore_case = ignore_case + + def _check_iterable(self, iterable, filter_name): + if not iterable: + pass + elif not isinstance(iterable, str) and not isinstance(iterable, list) and not isinstance(iterable, tuple): + raise ValueError(f"Incorrect value of {filter_name!r}") + elif isinstance(iterable, str): + iterable = [iterable] + elif isinstance(iterable, list) or isinstance(iterable, tuple): + iterable = [i for i in iterable if isinstance(i, str)] + return iterable + + async def check(self, obj: Union[types.Message, types.CallbackQuery, types.InlineQuery, types.Poll]): + + if isinstance(obj, types.Poll): + text = obj.question + elif isinstance(obj, types.Message): + text = obj.text or obj.caption + elif isinstance(obj, types.CallbackQuery): + text = obj.data + elif isinstance(obj, types.InlineQuery): + text = obj.query + else: + return False + + if self.ignore_case: + text = text.lower() + prepare_func = lambda string: str(string).lower() + else: + prepare_func = str + + if self.equals: + result = prepare_func(self.equals) == text + if result: + return True + elif not result and not any((self.contains, self.starts_with, self.ends_with)): + return False + + if self.contains: + result = any([prepare_func(i) in text for i in self.contains]) + if result: + return True + elif not result and not any((self.starts_with, self.ends_with)): + return False + + if self.starts_with: + result = any([text.startswith(prepare_func(i)) for i in self.starts_with]) + if result: + return True + elif not result and not self.ends_with: + return False + + if self.ends_with: + return any([text.endswith(prepare_func(i)) for i in self.ends_with]) + + return False + + +class TextMatchFilter(AdvancedCustomFilter): + """ + Filter to check Text message. + key: text + + Example: + @bot.message_handler(text=['account']) + """ + + key = 'text' + + async def check(self, message, text): + if isinstance(text, TextFilter): + return await text.check(message) + elif type(text) is list: + return message.text in text + else: + return text == message.text + + +class TextContainsFilter(AdvancedCustomFilter): + """ + Filter to check Text message. + key: text + + Example: + # Will respond if any message.text contains word 'account' + @bot.message_handler(text_contains=['account']) + """ + + key = 'text_contains' + + async def check(self, message, text): + if not isinstance(text, str) and not isinstance(text, list) and not isinstance(text, tuple): + raise ValueError("Incorrect text_contains value") + elif isinstance(text, str): + text = [text] + elif isinstance(text, list) or isinstance(text, tuple): + text = [i for i in text if isinstance(i, str)] + + return any([i in message.text for i in text]) + + +class TextStartsFilter(AdvancedCustomFilter): + """ + Filter to check whether message starts with some text. + + Example: + # Will work if message.text starts with 'Sir'. + @bot.message_handler(text_startswith='Sir') + """ + + key = 'text_startswith' + + async def check(self, message, text): + return message.text.startswith(text) + + +class ChatFilter(AdvancedCustomFilter): + """ + Check whether chat_id corresponds to given chat_id. + + Example: + @bot.message_handler(chat_id=[99999]) + """ + + key = 'chat_id' + + async def check(self, message, text): + return message.chat.id in text + + +class ForwardFilter(SimpleCustomFilter): + """ + Check whether message was forwarded from channel or group. + + Example: + + @bot.message_handler(is_forwarded=True) + """ + + key = 'is_forwarded' + + async def check(self, message): + return message.forward_from_chat is not None + + +class IsReplyFilter(SimpleCustomFilter): + """ + Check whether message is a reply. + + Example: + + @bot.message_handler(is_reply=True) + """ + + key = 'is_reply' + + async def check(self, message): + return message.reply_to_message is not None + + +class LanguageFilter(AdvancedCustomFilter): + """ + Check users language_code. + + Example: + + @bot.message_handler(language_code=['ru']) + """ + + key = 'language_code' + + async def check(self, message, text): + if type(text) is list: + return message.from_user.language_code in text + else: + return message.from_user.language_code == text + + +class IsAdminFilter(SimpleCustomFilter): + """ + Check whether the user is administrator / owner of the chat. + + Example: + @bot.message_handler(chat_types=['supergroup'], is_chat_admin=True) + """ + + key = 'is_chat_admin' + + def __init__(self, bot): + self._bot = bot + + async def check(self, message): + result = await self._bot.get_chat_member(message.chat.id, message.from_user.id) + return result.status in ['creator', 'administrator'] + + +class StateFilter(AdvancedCustomFilter): + """ + Filter to check state. + + Example: + @bot.message_handler(state=1) + """ + + def __init__(self, bot): + self.bot = bot + + key = 'state' + + async def check(self, message, text): + if text == '*': return True + + # needs to work with callbackquery + if isinstance(message, types.Message): + chat_id = message.chat.id + user_id = message.from_user.id + + if isinstance(message, types.CallbackQuery): + + chat_id = message.message.chat.id + user_id = message.from_user.id + message = message.message + + + if isinstance(text, list): + new_text = [] + for i in text: + if isinstance(i, State): i = i.name + new_text.append(i) + text = new_text + elif isinstance(text, State): + text = text.name + + if message.chat.type == 'group': + group_state = await self.bot.current_states.get_state(user_id, chat_id) + if group_state == text: + return True + elif group_state in text and type(text) is list: + return True + + + else: + user_state = await self.bot.current_states.get_state(user_id, chat_id) + if user_state == text: + return True + elif type(text) is list and user_state in text: + return True + + +class IsDigitFilter(SimpleCustomFilter): + """ + Filter to check whether the string is made up of only digits. + + Example: + @bot.message_handler(is_digit=True) + """ + key = 'is_digit' + + async def check(self, message): + return message.text.isdigit() diff --git a/telebot/asyncio_handler_backends.py b/telebot/asyncio_handler_backends.py new file mode 100644 index 0000000..e5e2e4f --- /dev/null +++ b/telebot/asyncio_handler_backends.py @@ -0,0 +1,56 @@ +class BaseMiddleware: + """ + Base class for middleware. + Your middlewares should be inherited from this class. + """ + + def __init__(self): + pass + + async def pre_process(self, message, data): + raise NotImplementedError + + async def post_process(self, message, data, exception): + raise NotImplementedError + + +class State: + def __init__(self) -> None: + self.name = None + + def __str__(self) -> str: + return self.name + + +class StatesGroup: + def __init_subclass__(cls) -> None: + + for name, value in cls.__dict__.items(): + if not name.startswith('__') and not callable(value) and isinstance(value, State): + # change value of that variable + value.name = ':'.join((cls.__name__, name)) + + +class SkipHandler: + """ + Class for skipping handlers. + Just return instance of this class + in middleware to skip handler. + Update will go to post_process, + but will skip execution of handler. + """ + + def __init__(self) -> None: + pass + +class CancelUpdate: + """ + Class for canceling updates. + Just return instance of this class + in middleware to skip update. + Update will skip handler and execution + of post_process in middlewares. + """ + + def __init__(self) -> None: + pass \ No newline at end of file diff --git a/telebot/asyncio_helper.py b/telebot/asyncio_helper.py new file mode 100644 index 0000000..591410c --- /dev/null +++ b/telebot/asyncio_helper.py @@ -0,0 +1,1741 @@ +import asyncio # for future uses +import aiohttp +from telebot import types + +try: + import ujson as json +except ImportError: + import json +import os +API_URL = 'https://api.telegram.org/bot{0}/{1}' + +from datetime import datetime + +import telebot +from telebot import util, logger + + +proxy = None +session = None + +FILE_URL = None + +CONNECT_TIMEOUT = 15 +READ_TIMEOUT = 30 + +LONG_POLLING_TIMEOUT = 10 # Should be positive, short polling should be used for testing purposes only (https://core.telegram.org/bots/api#getupdates) +REQUEST_TIMEOUT = 10 +MAX_RETRIES = 3 +logger = telebot.logger + + +REQUEST_LIMIT = 50 + +class SessionManager: + def __init__(self) -> None: + self.session = aiohttp.ClientSession(connector=aiohttp.TCPConnector(limit=REQUEST_LIMIT)) + + async def create_session(self): + self.session = aiohttp.ClientSession(connector=aiohttp.TCPConnector(limit=REQUEST_LIMIT)) + return self.session + + async def get_session(self): + if self.session.closed: + self.session = await self.create_session() + + # noinspection PyProtectedMember + if not self.session._loop.is_running(): + await self.session.close() + self.session = await self.create_session() + return self.session + + +session_manager = SessionManager() + +async def _process_request(token, url, method='get', params=None, files=None, request_timeout=None): + params = prepare_data(params, files) + if request_timeout is None: + request_timeout = REQUEST_TIMEOUT + timeout = aiohttp.ClientTimeout(total=request_timeout) + got_result = False + current_try=0 + session = await session_manager.get_session() + while not got_result and current_try 0: + ret = ret[:-1] + return '[' + ret + ']' + + + +async def _convert_entites(entites): + if entites is None: + return None + elif len(entites) == 0: + return [] + elif isinstance(entites[0], types.JsonSerializable): + return [entity.to_json() for entity in entites] + else: + return entites + + +async def _convert_poll_options(poll_options): + if poll_options is None: + return None + elif len(poll_options) == 0: + return [] + elif isinstance(poll_options[0], str): + # Compatibility mode with previous bug when only list of string was accepted as poll_options + return poll_options + elif isinstance(poll_options[0], types.PollOption): + return [option.text for option in poll_options] + else: + return poll_options + + +async def convert_input_media(media): + if isinstance(media, types.InputMedia): + return media.convert_input_media() + return None, None + + +async def convert_input_media_array(array): + media = [] + files = {} + for input_media in array: + if isinstance(input_media, types.InputMedia): + media_dict = input_media.to_dict() + if media_dict['media'].startswith('attach://'): + key = media_dict['media'].replace('attach://', '') + files[key] = input_media.media + media.append(media_dict) + return json.dumps(media), files + + +async def _no_encode(func): + def wrapper(key, val): + if key == 'filename': + return u'{0}={1}'.format(key, val) + else: + return func(key, val) + + return wrapper + +async def stop_poll(token, chat_id, message_id, reply_markup=None): + method_url = r'stopPoll' + payload = {'chat_id': str(chat_id), 'message_id': message_id} + if reply_markup: + payload['reply_markup'] = await _convert_markup(reply_markup) + return await _process_request(token, method_url, params=payload) + +# exceptions +class ApiException(Exception): + """ + This class represents a base Exception thrown when a call to the Telegram API fails. + In addition to an informative message, it has a `function_name` and a `result` attribute, which respectively + contain the name of the failed function and the returned result that made the function to be considered as + failed. + """ + + def __init__(self, msg, function_name, result): + super(ApiException, self).__init__("A request to the Telegram API was unsuccessful. {0}".format(msg)) + self.function_name = function_name + self.result = result + +class ApiHTTPException(ApiException): + """ + This class represents an Exception thrown when a call to the + Telegram API server returns HTTP code that is not 200. + """ + def __init__(self, function_name, result): + super(ApiHTTPException, self).__init__( + "The server returned HTTP {0} {1}. Response body:\n[{2}]" \ + .format(result.status_code, result.reason, result), + function_name, + result) + +class ApiInvalidJSONException(ApiException): + """ + This class represents an Exception thrown when a call to the + Telegram API server returns invalid json. + """ + def __init__(self, function_name, result): + super(ApiInvalidJSONException, self).__init__( + "The server returned an invalid JSON response. Response body:\n[{0}]" \ + .format(result), + function_name, + result) + +class ApiTelegramException(ApiException): + """ + This class represents an Exception thrown when a Telegram API returns error code. + """ + def __init__(self, function_name, result, result_json): + super(ApiTelegramException, self).__init__( + "Error code: {0}. Description: {1}" \ + .format(result_json['error_code'], result_json['description']), + function_name, + result) + self.result_json = result_json + self.error_code = result_json['error_code'] + +class RequestTimeout(Exception): + """ + This class represents a request timeout. + """ + pass \ No newline at end of file diff --git a/telebot/asyncio_storage/__init__.py b/telebot/asyncio_storage/__init__.py new file mode 100644 index 0000000..892f0af --- /dev/null +++ b/telebot/asyncio_storage/__init__.py @@ -0,0 +1,13 @@ +from telebot.asyncio_storage.memory_storage import StateMemoryStorage +from telebot.asyncio_storage.redis_storage import StateRedisStorage +from telebot.asyncio_storage.pickle_storage import StatePickleStorage +from telebot.asyncio_storage.base_storage import StateContext,StateStorageBase + + + + + +__all__ = [ + 'StateStorageBase', 'StateContext', + 'StateMemoryStorage', 'StateRedisStorage', 'StatePickleStorage' +] \ No newline at end of file diff --git a/telebot/asyncio_storage/base_storage.py b/telebot/asyncio_storage/base_storage.py new file mode 100644 index 0000000..38615c4 --- /dev/null +++ b/telebot/asyncio_storage/base_storage.py @@ -0,0 +1,68 @@ +import copy + + +class StateStorageBase: + def __init__(self) -> None: + pass + + async def set_data(self, chat_id, user_id, key, value): + """ + Set data for a user in a particular chat. + """ + raise NotImplementedError + + async def get_data(self, chat_id, user_id): + """ + Get data for a user in a particular chat. + """ + raise NotImplementedError + + async def set_state(self, chat_id, user_id, state): + """ + Set state for a particular user. + + ! Note that you should create a + record if it does not exist, and + if a record with state already exists, + you need to update a record. + """ + raise NotImplementedError + + async def delete_state(self, chat_id, user_id): + """ + Delete state for a particular user. + """ + raise NotImplementedError + + async def reset_data(self, chat_id, user_id): + """ + Reset data for a particular user in a chat. + """ + raise NotImplementedError + + async def get_state(self, chat_id, user_id): + raise NotImplementedError + + async def save(self, chat_id, user_id, data): + raise NotImplementedError + + +class StateContext: + """ + Class for data. + """ + + def __init__(self, obj, chat_id, user_id): + self.obj = obj + self.data = None + self.chat_id = chat_id + self.user_id = user_id + + + + async def __aenter__(self): + self.data = copy.deepcopy(await self.obj.get_data(self.chat_id, self.user_id)) + return self.data + + async def __aexit__(self, exc_type, exc_val, exc_tb): + return await self.obj.save(self.chat_id, self.user_id, self.data) \ No newline at end of file diff --git a/telebot/asyncio_storage/memory_storage.py b/telebot/asyncio_storage/memory_storage.py new file mode 100644 index 0000000..58a6e35 --- /dev/null +++ b/telebot/asyncio_storage/memory_storage.py @@ -0,0 +1,66 @@ +from telebot.asyncio_storage.base_storage import StateStorageBase, StateContext + +class StateMemoryStorage(StateStorageBase): + def __init__(self) -> None: + self.data = {} + # + # {chat_id: {user_id: {'state': None, 'data': {}}, ...}, ...} + + + async def set_state(self, chat_id, user_id, state): + if isinstance(state, object): + state = state.name + if chat_id in self.data: + if user_id in self.data[chat_id]: + self.data[chat_id][user_id]['state'] = state + return True + else: + self.data[chat_id][user_id] = {'state': state, 'data': {}} + return True + self.data[chat_id] = {user_id: {'state': state, 'data': {}}} + return True + + async def delete_state(self, chat_id, user_id): + if self.data.get(chat_id): + if self.data[chat_id].get(user_id): + del self.data[chat_id][user_id] + if chat_id == user_id: + del self.data[chat_id] + + return True + + return False + + + async def get_state(self, chat_id, user_id): + if self.data.get(chat_id): + if self.data[chat_id].get(user_id): + return self.data[chat_id][user_id]['state'] + + return None + async def get_data(self, chat_id, user_id): + if self.data.get(chat_id): + if self.data[chat_id].get(user_id): + return self.data[chat_id][user_id]['data'] + + return None + + async def reset_data(self, chat_id, user_id): + if self.data.get(chat_id): + if self.data[chat_id].get(user_id): + self.data[chat_id][user_id]['data'] = {} + return True + return False + + async def set_data(self, chat_id, user_id, key, value): + if self.data.get(chat_id): + if self.data[chat_id].get(user_id): + self.data[chat_id][user_id]['data'][key] = value + return True + raise RuntimeError('chat_id {} and user_id {} does not exist'.format(chat_id, user_id)) + + def get_interactive_data(self, chat_id, user_id): + return StateContext(self, chat_id, user_id) + + async def save(self, chat_id, user_id, data): + self.data[chat_id][user_id]['data'] = data \ No newline at end of file diff --git a/telebot/asyncio_storage/pickle_storage.py b/telebot/asyncio_storage/pickle_storage.py new file mode 100644 index 0000000..dd6419e --- /dev/null +++ b/telebot/asyncio_storage/pickle_storage.py @@ -0,0 +1,109 @@ +from telebot.asyncio_storage.base_storage import StateStorageBase, StateContext +import os + + +import pickle + + +class StatePickleStorage(StateStorageBase): + def __init__(self, file_path="./.state-save/states.pkl") -> None: + self.file_path = file_path + self.create_dir() + self.data = self.read() + + async def convert_old_to_new(self): + # old looks like: + # {1: {'state': 'start', 'data': {'name': 'John'}} + # we should update old version pickle to new. + # new looks like: + # {1: {2: {'state': 'start', 'data': {'name': 'John'}}}} + new_data = {} + for key, value in self.data.items(): + # this returns us id and dict with data and state + new_data[key] = {key: value} # convert this to new + # pass it to global data + self.data = new_data + self.update_data() # update data in file + + def create_dir(self): + """ + Create directory .save-handlers. + """ + dirs = self.file_path.rsplit('/', maxsplit=1)[0] + os.makedirs(dirs, exist_ok=True) + if not os.path.isfile(self.file_path): + with open(self.file_path,'wb') as file: + pickle.dump({}, file) + + def read(self): + file = open(self.file_path, 'rb') + data = pickle.load(file) + file.close() + return data + + def update_data(self): + file = open(self.file_path, 'wb+') + pickle.dump(self.data, file, protocol=pickle.HIGHEST_PROTOCOL) + file.close() + + async def set_state(self, chat_id, user_id, state): + if isinstance(state, object): + state = state.name + if chat_id in self.data: + if user_id in self.data[chat_id]: + self.data[chat_id][user_id]['state'] = state + return True + else: + self.data[chat_id][user_id] = {'state': state, 'data': {}} + return True + self.data[chat_id] = {user_id: {'state': state, 'data': {}}} + self.update_data() + return True + + async def delete_state(self, chat_id, user_id): + if self.data.get(chat_id): + if self.data[chat_id].get(user_id): + del self.data[chat_id][user_id] + if chat_id == user_id: + del self.data[chat_id] + self.update_data() + return True + + return False + + + async def get_state(self, chat_id, user_id): + if self.data.get(chat_id): + if self.data[chat_id].get(user_id): + return self.data[chat_id][user_id]['state'] + + return None + async def get_data(self, chat_id, user_id): + if self.data.get(chat_id): + if self.data[chat_id].get(user_id): + return self.data[chat_id][user_id]['data'] + + return None + + async def reset_data(self, chat_id, user_id): + if self.data.get(chat_id): + if self.data[chat_id].get(user_id): + self.data[chat_id][user_id]['data'] = {} + self.update_data() + return True + return False + + async def set_data(self, chat_id, user_id, key, value): + if self.data.get(chat_id): + if self.data[chat_id].get(user_id): + self.data[chat_id][user_id]['data'][key] = value + self.update_data() + return True + raise RuntimeError('chat_id {} and user_id {} does not exist'.format(chat_id, user_id)) + + def get_interactive_data(self, chat_id, user_id): + return StateContext(self, chat_id, user_id) + + async def save(self, chat_id, user_id, data): + self.data[chat_id][user_id]['data'] = data + self.update_data() \ No newline at end of file diff --git a/telebot/asyncio_storage/redis_storage.py b/telebot/asyncio_storage/redis_storage.py new file mode 100644 index 0000000..f2a2606 --- /dev/null +++ b/telebot/asyncio_storage/redis_storage.py @@ -0,0 +1,171 @@ +from telebot.asyncio_storage.base_storage import StateStorageBase, StateContext +import json + +redis_installed = True +try: + import aioredis +except: + redis_installed = False + + +class StateRedisStorage(StateStorageBase): + """ + This class is for Redis storage. + This will work only for states. + To use it, just pass this class to: + TeleBot(storage=StateRedisStorage()) + """ + def __init__(self, host='localhost', port=6379, db=0, password=None, prefix='telebot_'): + if not redis_installed: + raise ImportError('AioRedis is not installed. Install it via "pip install aioredis"') + + + aioredis_version = tuple(map(int, aioredis.__version__.split(".")[0])) + if aioredis_version < (2,): + raise ImportError('Invalid aioredis version. Aioredis version should be >= 2.0.0') + self.redis = aioredis.Redis(host=host, port=port, db=db, password=password) + + self.prefix = prefix + #self.con = Redis(connection_pool=self.redis) -> use this when necessary + # + # {chat_id: {user_id: {'state': None, 'data': {}}, ...}, ...} + + async def get_record(self, key): + """ + Function to get record from database. + It has nothing to do with states. + Made for backend compatibility + """ + result = await self.redis.get(self.prefix+str(key)) + if result: return json.loads(result) + return + + async def set_record(self, key, value): + """ + Function to set record to database. + It has nothing to do with states. + Made for backend compatibility + """ + + await self.redis.set(self.prefix+str(key), json.dumps(value)) + return True + + async def delete_record(self, key): + """ + Function to delete record from database. + It has nothing to do with states. + Made for backend compatibility + """ + await self.redis.delete(self.prefix+str(key)) + return True + + async def set_state(self, chat_id, user_id, state): + """ + Set state for a particular user in a chat. + """ + response = await self.get_record(chat_id) + user_id = str(user_id) + if isinstance(state, object): + state = state.name + if response: + if user_id in response: + response[user_id]['state'] = state + else: + response[user_id] = {'state': state, 'data': {}} + else: + response = {user_id: {'state': state, 'data': {}}} + await self.set_record(chat_id, response) + + return True + + async def delete_state(self, chat_id, user_id): + """ + Delete state for a particular user in a chat. + """ + response = await self.get_record(chat_id) + user_id = str(user_id) + if response: + if user_id in response: + del response[user_id] + if user_id == str(chat_id): + await self.delete_record(chat_id) + return True + else: await self.set_record(chat_id, response) + return True + return False + + async def get_value(self, chat_id, user_id, key): + """ + Get value for a data of a user in a chat. + """ + response = await self.get_record(chat_id) + user_id = str(user_id) + if response: + if user_id in response: + if key in response[user_id]['data']: + return response[user_id]['data'][key] + return None + + async def get_state(self, chat_id, user_id): + """ + Get state of a user in a chat. + """ + response = await self.get_record(chat_id) + user_id = str(user_id) + if response: + if user_id in response: + return response[user_id]['state'] + + return None + + async def get_data(self, chat_id, user_id): + """ + Get data of particular user in a particular chat. + """ + response = await self.get_record(chat_id) + user_id = str(user_id) + if response: + if user_id in response: + return response[user_id]['data'] + return None + + async def reset_data(self, chat_id, user_id): + """ + Reset data of a user in a chat. + """ + response = await self.get_record(chat_id) + user_id = str(user_id) + if response: + if user_id in response: + response[user_id]['data'] = {} + await self.set_record(chat_id, response) + return True + + async def set_data(self, chat_id, user_id, key, value): + """ + Set data without interactive data. + """ + response = await self.get_record(chat_id) + user_id = str(user_id) + if response: + if user_id in response: + response[user_id]['data'][key] = value + await self.set_record(chat_id, response) + return True + return False + + def get_interactive_data(self, chat_id, user_id): + """ + Get Data in interactive way. + You can use with() with this function. + """ + return StateContext(self, chat_id, user_id) + + async def save(self, chat_id, user_id, data): + response = await self.get_record(chat_id) + user_id = str(user_id) + if response: + if user_id in response: + response[user_id]['data'] = dict(data, **response[user_id]['data']) + await self.set_record(chat_id, response) + return True diff --git a/telebot/callback_data.py b/telebot/callback_data.py new file mode 100644 index 0000000..ecbe81e --- /dev/null +++ b/telebot/callback_data.py @@ -0,0 +1,115 @@ +import typing + + +class CallbackDataFilter: + + def __init__(self, factory, config: typing.Dict[str, str]): + self.config = config + self.factory = factory + + def check(self, query): + """ + Checks if query.data appropriates to specified config + :param query: telebot.types.CallbackQuery + :return: bool + """ + + try: + data = self.factory.parse(query.data) + except ValueError: + return False + + for key, value in self.config.items(): + if isinstance(value, (list, tuple, set, frozenset)): + if data.get(key) not in value: + return False + elif data.get(key) != value: + return False + return True + + +class CallbackData: + """ + Callback data factory + This class will help you to work with CallbackQuery + """ + + def __init__(self, *parts, prefix: str, sep=':'): + if not isinstance(prefix, str): + raise TypeError(f'Prefix must be instance of str not {type(prefix).__name__}') + if not prefix: + raise ValueError("Prefix can't be empty") + if sep in prefix: + raise ValueError(f"Separator {sep!r} can't be used in prefix") + + self.prefix = prefix + self.sep = sep + + self._part_names = parts + + def new(self, *args, **kwargs) -> str: + """ + Generate callback data + :param args: positional parameters of CallbackData instance parts + :param kwargs: named parameters + :return: str + """ + args = list(args) + + data = [self.prefix] + + for part in self._part_names: + value = kwargs.pop(part, None) + if value is None: + if args: + value = args.pop(0) + else: + raise ValueError(f'Value for {part!r} was not passed!') + + if value is not None and not isinstance(value, str): + value = str(value) + + if self.sep in value: + raise ValueError(f"Symbol {self.sep!r} is defined as the separator and can't be used in parts' values") + + data.append(value) + + if args or kwargs: + raise TypeError('Too many arguments were passed!') + + callback_data = self.sep.join(data) + + if len(callback_data.encode()) > 64: + raise ValueError('Resulted callback data is too long!') + + return callback_data + + def parse(self, callback_data: str) -> typing.Dict[str, str]: + """ + Parse data from the callback data + :param callback_data: string, use to telebot.types.CallbackQuery to parse it from string to a dict + :return: dict parsed from callback data + """ + + prefix, *parts = callback_data.split(self.sep) + if prefix != self.prefix: + raise ValueError("Passed callback data can't be parsed with that prefix.") + elif len(parts) != len(self._part_names): + raise ValueError('Invalid parts count!') + + result = {'@': prefix} + result.update(zip(self._part_names, parts)) + return result + + def filter(self, **config) -> CallbackDataFilter: + """ + Generate filter + + :param config: specified named parameters will be checked with CallbackQury.data + :return: CallbackDataFilter class + """ + + for key in config.keys(): + if key not in self._part_names: + raise ValueError(f'Invalid field name {key!r}') + return CallbackDataFilter(self, config) diff --git a/telebot/custom_filters.py b/telebot/custom_filters.py new file mode 100644 index 0000000..8442be4 --- /dev/null +++ b/telebot/custom_filters.py @@ -0,0 +1,336 @@ +from abc import ABC +from typing import Optional, Union +from telebot.handler_backends import State + +from telebot import types + + + + +class SimpleCustomFilter(ABC): + """ + Simple Custom Filter base class. + Create child class with check() method. + Accepts only message, returns bool value, that is compared with given in handler. + """ + + def check(self, message): + """ + Perform a check. + """ + pass + + +class AdvancedCustomFilter(ABC): + """ + Simple Custom Filter base class. + Create child class with check() method. + Accepts two parameters, returns bool: True - filter passed, False - filter failed. + message: Message class + text: Filter value given in handler + """ + + def check(self, message, text): + """ + Perform a check. + """ + pass + + +class TextFilter: + """ + Advanced text filter to check (types.Message, types.CallbackQuery, types.InlineQuery, types.Poll) + + example of usage is in examples/custom_filters/advanced_text_filter.py + """ + + def __init__(self, + equals: Optional[str] = None, + contains: Optional[Union[list, tuple]] = None, + starts_with: Optional[Union[str, list, tuple]] = None, + ends_with: Optional[Union[str, list, tuple]] = None, + ignore_case: bool = False): + + """ + :param equals: string, True if object's text is equal to passed string + :param contains: list[str] or tuple[str], True if any string element of iterable is in text + :param starts_with: string, True if object's text starts with passed string + :param ends_with: string, True if object's text starts with passed string + :param ignore_case: bool (default False), case insensitive + """ + + to_check = sum((pattern is not None for pattern in (equals, contains, starts_with, ends_with))) + if to_check == 0: + raise ValueError('None of the check modes was specified') + + self.equals = equals + self.contains = self._check_iterable(contains, filter_name='contains') + self.starts_with = self._check_iterable(starts_with, filter_name='starts_with') + self.ends_with = self._check_iterable(ends_with, filter_name='ends_with') + self.ignore_case = ignore_case + + def _check_iterable(self, iterable, filter_name: str): + if not iterable: + pass + elif not isinstance(iterable, str) and not isinstance(iterable, list) and not isinstance(iterable, tuple): + raise ValueError(f"Incorrect value of {filter_name!r}") + elif isinstance(iterable, str): + iterable = [iterable] + elif isinstance(iterable, list) or isinstance(iterable, tuple): + iterable = [i for i in iterable if isinstance(i, str)] + return iterable + + def check(self, obj: Union[types.Message, types.CallbackQuery, types.InlineQuery, types.Poll]): + + if isinstance(obj, types.Poll): + text = obj.question + elif isinstance(obj, types.Message): + text = obj.text or obj.caption + elif isinstance(obj, types.CallbackQuery): + text = obj.data + elif isinstance(obj, types.InlineQuery): + text = obj.query + else: + return False + + if self.ignore_case: + text = text.lower() + + if self.equals: + self.equals = self.equals.lower() + elif self.contains: + self.contains = tuple(map(str.lower, self.contains)) + elif self.starts_with: + self.starts_with = tuple(map(str.lower, self.starts_with)) + elif self.ends_with: + self.ends_with = tuple(map(str.lower, self.ends_with)) + + if self.equals: + result = self.equals == text + if result: + return True + elif not result and not any((self.contains, self.starts_with, self.ends_with)): + return False + + if self.contains: + result = any([i in text for i in self.contains]) + if result: + return True + elif not result and not any((self.starts_with, self.ends_with)): + return False + + if self.starts_with: + result = any([text.startswith(i) for i in self.starts_with]) + if result: + return True + elif not result and not self.ends_with: + return False + + if self.ends_with: + return any([text.endswith(i) for i in self.ends_with]) + + return False + +class TextMatchFilter(AdvancedCustomFilter): + """ + Filter to check Text message. + key: text + + Example: + @bot.message_handler(text=['account']) + """ + + key = 'text' + + def check(self, message, text): + if isinstance(text, TextFilter): + return text.check(message) + elif type(text) is list: + return message.text in text + else: + return text == message.text + + +class TextContainsFilter(AdvancedCustomFilter): + """ + Filter to check Text message. + key: text + + Example: + # Will respond if any message.text contains word 'account' + @bot.message_handler(text_contains=['account']) + """ + + key = 'text_contains' + + def check(self, message, text): + if not isinstance(text, str) and not isinstance(text, list) and not isinstance(text, tuple): + raise ValueError("Incorrect text_contains value") + elif isinstance(text, str): + text = [text] + elif isinstance(text, list) or isinstance(text, tuple): + text = [i for i in text if isinstance(i, str)] + + return any([i in message.text for i in text]) + + +class TextStartsFilter(AdvancedCustomFilter): + """ + Filter to check whether message starts with some text. + + Example: + # Will work if message.text starts with 'Sir'. + @bot.message_handler(text_startswith='Sir') + """ + + key = 'text_startswith' + + def check(self, message, text): + return message.text.startswith(text) + + +class ChatFilter(AdvancedCustomFilter): + """ + Check whether chat_id corresponds to given chat_id. + + Example: + @bot.message_handler(chat_id=[99999]) + """ + + key = 'chat_id' + + def check(self, message, text): + return message.chat.id in text + + +class ForwardFilter(SimpleCustomFilter): + """ + Check whether message was forwarded from channel or group. + + Example: + + @bot.message_handler(is_forwarded=True) + """ + + key = 'is_forwarded' + + def check(self, message): + return message.forward_from_chat is not None + + +class IsReplyFilter(SimpleCustomFilter): + """ + Check whether message is a reply. + + Example: + + @bot.message_handler(is_reply=True) + """ + + key = 'is_reply' + + def check(self, message): + return message.reply_to_message is not None + + +class LanguageFilter(AdvancedCustomFilter): + """ + Check users language_code. + + Example: + + @bot.message_handler(language_code=['ru']) + """ + + key = 'language_code' + + def check(self, message, text): + if type(text) is list: + return message.from_user.language_code in text + else: + return message.from_user.language_code == text + + +class IsAdminFilter(SimpleCustomFilter): + """ + Check whether the user is administrator / owner of the chat. + + Example: + @bot.message_handler(chat_types=['supergroup'], is_chat_admin=True) + """ + + key = 'is_chat_admin' + + def __init__(self, bot): + self._bot = bot + + def check(self, message): + return self._bot.get_chat_member(message.chat.id, message.from_user.id).status in ['creator', 'administrator'] + + +class StateFilter(AdvancedCustomFilter): + """ + Filter to check state. + + Example: + @bot.message_handler(state=1) + """ + + def __init__(self, bot): + self.bot = bot + + key = 'state' + + def check(self, message, text): + if text == '*': return True + + # needs to work with callbackquery + if isinstance(message, types.Message): + chat_id = message.chat.id + user_id = message.from_user.id + + if isinstance(message, types.CallbackQuery): + + chat_id = message.message.chat.id + user_id = message.from_user.id + message = message.message + + + + + if isinstance(text, list): + new_text = [] + for i in text: + if isinstance(i, State): i = i.name + new_text.append(i) + text = new_text + elif isinstance(text, State): + text = text.name + + if message.chat.type == 'group': + group_state = self.bot.current_states.get_state(user_id, chat_id) + if group_state == text: + return True + elif group_state in text and type(text) is list: + return True + + + else: + user_state = self.bot.current_states.get_state(user_id, chat_id) + if user_state == text: + return True + elif type(text) is list and user_state in text: + return True + + +class IsDigitFilter(SimpleCustomFilter): + """ + Filter to check whether the string is made up of only digits. + + Example: + @bot.message_handler(is_digit=True) + """ + key = 'is_digit' + + def check(self, message): + return message.text.isdigit() diff --git a/telebot/handler_backends.py b/telebot/handler_backends.py new file mode 100644 index 0000000..b838231 --- /dev/null +++ b/telebot/handler_backends.py @@ -0,0 +1,206 @@ +import os +import pickle +import threading + +from telebot import apihelper +try: + from redis import Redis + redis_installed = True +except: + redis_installed = False + + +class HandlerBackend(object): + """ + Class for saving (next step|reply) handlers + """ + def __init__(self, handlers=None): + if handlers is None: + handlers = {} + self.handlers = handlers + + def register_handler(self, handler_group_id, handler): + raise NotImplementedError() + + def clear_handlers(self, handler_group_id): + raise NotImplementedError() + + def get_handlers(self, handler_group_id): + raise NotImplementedError() + + +class MemoryHandlerBackend(HandlerBackend): + def register_handler(self, handler_group_id, handler): + if handler_group_id in self.handlers: + self.handlers[handler_group_id].append(handler) + else: + self.handlers[handler_group_id] = [handler] + + def clear_handlers(self, handler_group_id): + self.handlers.pop(handler_group_id, None) + + def get_handlers(self, handler_group_id): + return self.handlers.pop(handler_group_id, None) + + def load_handlers(self, filename, del_file_after_loading): + raise NotImplementedError() + + +class FileHandlerBackend(HandlerBackend): + def __init__(self, handlers=None, filename='./.handler-saves/handlers.save', delay=120): + super(FileHandlerBackend, self).__init__(handlers) + self.filename = filename + self.delay = delay + self.timer = threading.Timer(delay, self.save_handlers) + + def register_handler(self, handler_group_id, handler): + if handler_group_id in self.handlers: + self.handlers[handler_group_id].append(handler) + else: + self.handlers[handler_group_id] = [handler] + self.start_save_timer() + + def clear_handlers(self, handler_group_id): + self.handlers.pop(handler_group_id, None) + self.start_save_timer() + + def get_handlers(self, handler_group_id): + handlers = self.handlers.pop(handler_group_id, None) + self.start_save_timer() + return handlers + + def start_save_timer(self): + if not self.timer.is_alive(): + if self.delay <= 0: + self.save_handlers() + else: + self.timer = threading.Timer(self.delay, self.save_handlers) + self.timer.start() + + def save_handlers(self): + self.dump_handlers(self.handlers, self.filename) + + def load_handlers(self, filename=None, del_file_after_loading=True): + if not filename: + filename = self.filename + tmp = self.return_load_handlers(filename, del_file_after_loading=del_file_after_loading) + if tmp is not None: + self.handlers.update(tmp) + + @staticmethod + def dump_handlers(handlers, filename, file_mode="wb"): + dirs = filename.rsplit('/', maxsplit=1)[0] + os.makedirs(dirs, exist_ok=True) + + with open(filename + ".tmp", file_mode) as file: + if (apihelper.CUSTOM_SERIALIZER is None): + pickle.dump(handlers, file) + else: + apihelper.CUSTOM_SERIALIZER.dump(handlers, file) + + if os.path.isfile(filename): + os.remove(filename) + + os.rename(filename + ".tmp", filename) + + @staticmethod + def return_load_handlers(filename, del_file_after_loading=True): + if os.path.isfile(filename) and os.path.getsize(filename) > 0: + with open(filename, "rb") as file: + if (apihelper.CUSTOM_SERIALIZER is None): + handlers = pickle.load(file) + else: + handlers = apihelper.CUSTOM_SERIALIZER.load(file) + + if del_file_after_loading: + os.remove(filename) + + return handlers + + +class RedisHandlerBackend(HandlerBackend): + def __init__(self, handlers=None, host='localhost', port=6379, db=0, prefix='telebot', password=None): + super(RedisHandlerBackend, self).__init__(handlers) + if not redis_installed: + raise Exception("Redis is not installed. Install it via 'pip install redis'") + self.prefix = prefix + self.redis = Redis(host, port, db, password) + + def _key(self, handle_group_id): + return ':'.join((self.prefix, str(handle_group_id))) + + def register_handler(self, handler_group_id, handler): + handlers = [] + value = self.redis.get(self._key(handler_group_id)) + if value: + handlers = pickle.loads(value) + handlers.append(handler) + self.redis.set(self._key(handler_group_id), pickle.dumps(handlers)) + + def clear_handlers(self, handler_group_id): + self.redis.delete(self._key(handler_group_id)) + + def get_handlers(self, handler_group_id): + handlers = None + value = self.redis.get(self._key(handler_group_id)) + if value: + handlers = pickle.loads(value) + self.clear_handlers(handler_group_id) + return handlers + + +class State: + def __init__(self) -> None: + self.name = None + def __str__(self) -> str: + return self.name + + + +class StatesGroup: + def __init_subclass__(cls) -> None: + for name, value in cls.__dict__.items(): + if not name.startswith('__') and not callable(value) and isinstance(value, State): + # change value of that variable + value.name = ':'.join((cls.__name__, name)) + + +class BaseMiddleware: + """ + Base class for middleware. + Your middlewares should be inherited from this class. + """ + + def __init__(self): + pass + + def pre_process(self, message, data): + raise NotImplementedError + + def post_process(self, message, data, exception): + raise NotImplementedError + + +class SkipHandler: + """ + Class for skipping handlers. + Just return instance of this class + in middleware to skip handler. + Update will go to post_process, + but will skip execution of handler. + """ + + def __init__(self) -> None: + pass + +class CancelUpdate: + """ + Class for canceling updates. + Just return instance of this class + in middleware to skip update. + Update will skip handler and execution + of post_process in middlewares. + """ + + def __init__(self) -> None: + pass \ No newline at end of file diff --git a/telebot/storage/__init__.py b/telebot/storage/__init__.py new file mode 100644 index 0000000..59e2b05 --- /dev/null +++ b/telebot/storage/__init__.py @@ -0,0 +1,13 @@ +from telebot.storage.memory_storage import StateMemoryStorage +from telebot.storage.redis_storage import StateRedisStorage +from telebot.storage.pickle_storage import StatePickleStorage +from telebot.storage.base_storage import StateContext,StateStorageBase + + + + + +__all__ = [ + 'StateStorageBase', 'StateContext', + 'StateMemoryStorage', 'StateRedisStorage', 'StatePickleStorage' +] \ No newline at end of file diff --git a/telebot/storage/__pycache__/__init__.cpython-39.pyc b/telebot/storage/__pycache__/__init__.cpython-39.pyc new file mode 100644 index 0000000..ed5cd05 Binary files /dev/null and b/telebot/storage/__pycache__/__init__.cpython-39.pyc differ diff --git a/telebot/storage/__pycache__/base_storage.cpython-39.pyc b/telebot/storage/__pycache__/base_storage.cpython-39.pyc new file mode 100644 index 0000000..4860da4 Binary files /dev/null and b/telebot/storage/__pycache__/base_storage.cpython-39.pyc differ diff --git a/telebot/storage/__pycache__/memory_storage.cpython-39.pyc b/telebot/storage/__pycache__/memory_storage.cpython-39.pyc new file mode 100644 index 0000000..ac93309 Binary files /dev/null and b/telebot/storage/__pycache__/memory_storage.cpython-39.pyc differ diff --git a/telebot/storage/__pycache__/pickle_storage.cpython-39.pyc b/telebot/storage/__pycache__/pickle_storage.cpython-39.pyc new file mode 100644 index 0000000..0bd6f63 Binary files /dev/null and b/telebot/storage/__pycache__/pickle_storage.cpython-39.pyc differ diff --git a/telebot/storage/__pycache__/redis_storage.cpython-39.pyc b/telebot/storage/__pycache__/redis_storage.cpython-39.pyc new file mode 100644 index 0000000..69ad714 Binary files /dev/null and b/telebot/storage/__pycache__/redis_storage.cpython-39.pyc differ diff --git a/telebot/storage/base_storage.py b/telebot/storage/base_storage.py new file mode 100644 index 0000000..bafd9a1 --- /dev/null +++ b/telebot/storage/base_storage.py @@ -0,0 +1,65 @@ +import copy + +class StateStorageBase: + def __init__(self) -> None: + pass + + def set_data(self, chat_id, user_id, key, value): + """ + Set data for a user in a particular chat. + """ + raise NotImplementedError + + def get_data(self, chat_id, user_id): + """ + Get data for a user in a particular chat. + """ + raise NotImplementedError + + def set_state(self, chat_id, user_id, state): + """ + Set state for a particular user. + + ! Note that you should create a + record if it does not exist, and + if a record with state already exists, + you need to update a record. + """ + raise NotImplementedError + + def delete_state(self, chat_id, user_id): + """ + Delete state for a particular user. + """ + raise NotImplementedError + + def reset_data(self, chat_id, user_id): + """ + Reset data for a particular user in a chat. + """ + raise NotImplementedError + + def get_state(self, chat_id, user_id): + raise NotImplementedError + + def save(self, chat_id, user_id, data): + raise NotImplementedError + + + +class StateContext: + """ + Class for data. + """ + def __init__(self , obj, chat_id, user_id) -> None: + self.obj = obj + self.data = copy.deepcopy(obj.get_data(chat_id, user_id)) + self.chat_id = chat_id + self.user_id = user_id + + + def __enter__(self): + return self.data + + def __exit__(self, exc_type, exc_val, exc_tb): + return self.obj.save(self.chat_id, self.user_id, self.data) \ No newline at end of file diff --git a/telebot/storage/memory_storage.py b/telebot/storage/memory_storage.py new file mode 100644 index 0000000..45d4da3 --- /dev/null +++ b/telebot/storage/memory_storage.py @@ -0,0 +1,67 @@ +from telebot.storage.base_storage import StateStorageBase, StateContext + +class StateMemoryStorage(StateStorageBase): + def __init__(self) -> None: + self.data = {} + # + # {chat_id: {user_id: {'state': None, 'data': {}}, ...}, ...} + + + def set_state(self, chat_id, user_id, state): + if isinstance(state, object): + state = state.name + if chat_id in self.data: + if user_id in self.data[chat_id]: + self.data[chat_id][user_id]['state'] = state + return True + else: + self.data[chat_id][user_id] = {'state': state, 'data': {}} + return True + self.data[chat_id] = {user_id: {'state': state, 'data': {}}} + return True + + def delete_state(self, chat_id, user_id): + if self.data.get(chat_id): + if self.data[chat_id].get(user_id): + del self.data[chat_id][user_id] + if chat_id == user_id: + del self.data[chat_id] + + return True + + return False + + + def get_state(self, chat_id, user_id): + + if self.data.get(chat_id): + if self.data[chat_id].get(user_id): + return self.data[chat_id][user_id]['state'] + + return None + def get_data(self, chat_id, user_id): + if self.data.get(chat_id): + if self.data[chat_id].get(user_id): + return self.data[chat_id][user_id]['data'] + + return None + + def reset_data(self, chat_id, user_id): + if self.data.get(chat_id): + if self.data[chat_id].get(user_id): + self.data[chat_id][user_id]['data'] = {} + return True + return False + + def set_data(self, chat_id, user_id, key, value): + if self.data.get(chat_id): + if self.data[chat_id].get(user_id): + self.data[chat_id][user_id]['data'][key] = value + return True + raise RuntimeError('chat_id {} and user_id {} does not exist'.format(chat_id, user_id)) + + def get_interactive_data(self, chat_id, user_id): + return StateContext(self, chat_id, user_id) + + def save(self, chat_id, user_id, data): + self.data[chat_id][user_id]['data'] = data \ No newline at end of file diff --git a/telebot/storage/pickle_storage.py b/telebot/storage/pickle_storage.py new file mode 100644 index 0000000..a273690 --- /dev/null +++ b/telebot/storage/pickle_storage.py @@ -0,0 +1,115 @@ +from telebot.storage.base_storage import StateStorageBase, StateContext +import os + + +import pickle + + +class StatePickleStorage(StateStorageBase): + # noinspection PyMissingConstructor + def __init__(self, file_path="./.state-save/states.pkl") -> None: + self.file_path = file_path + self.create_dir() + self.data = self.read() + + def convert_old_to_new(self): + """ + Use this function to convert old storage to new storage. + This function is for people who was using pickle storage + that was in version <=4.3.1. + """ + # old looks like: + # {1: {'state': 'start', 'data': {'name': 'John'}} + # we should update old version pickle to new. + # new looks like: + # {1: {2: {'state': 'start', 'data': {'name': 'John'}}}} + new_data = {} + for key, value in self.data.items(): + # this returns us id and dict with data and state + new_data[key] = {key: value} # convert this to new + # pass it to global data + self.data = new_data + self.update_data() # update data in file + + def create_dir(self): + """ + Create directory .save-handlers. + """ + dirs = self.file_path.rsplit('/', maxsplit=1)[0] + os.makedirs(dirs, exist_ok=True) + if not os.path.isfile(self.file_path): + with open(self.file_path,'wb') as file: + pickle.dump({}, file) + + def read(self): + file = open(self.file_path, 'rb') + data = pickle.load(file) + file.close() + return data + + def update_data(self): + file = open(self.file_path, 'wb+') + pickle.dump(self.data, file, protocol=pickle.HIGHEST_PROTOCOL) + file.close() + + def set_state(self, chat_id, user_id, state): + if isinstance(state, object): + state = state.name + if chat_id in self.data: + if user_id in self.data[chat_id]: + self.data[chat_id][user_id]['state'] = state + return True + else: + self.data[chat_id][user_id] = {'state': state, 'data': {}} + return True + self.data[chat_id] = {user_id: {'state': state, 'data': {}}} + self.update_data() + return True + + def delete_state(self, chat_id, user_id): + if self.data.get(chat_id): + if self.data[chat_id].get(user_id): + del self.data[chat_id][user_id] + if chat_id == user_id: + del self.data[chat_id] + self.update_data() + return True + + return False + + + def get_state(self, chat_id, user_id): + if self.data.get(chat_id): + if self.data[chat_id].get(user_id): + return self.data[chat_id][user_id]['state'] + + return None + def get_data(self, chat_id, user_id): + if self.data.get(chat_id): + if self.data[chat_id].get(user_id): + return self.data[chat_id][user_id]['data'] + + return None + + def reset_data(self, chat_id, user_id): + if self.data.get(chat_id): + if self.data[chat_id].get(user_id): + self.data[chat_id][user_id]['data'] = {} + self.update_data() + return True + return False + + def set_data(self, chat_id, user_id, key, value): + if self.data.get(chat_id): + if self.data[chat_id].get(user_id): + self.data[chat_id][user_id]['data'][key] = value + self.update_data() + return True + raise RuntimeError('chat_id {} and user_id {} does not exist'.format(chat_id, user_id)) + + def get_interactive_data(self, chat_id, user_id): + return StateContext(self, chat_id, user_id) + + def save(self, chat_id, user_id, data): + self.data[chat_id][user_id]['data'] = data + self.update_data() diff --git a/telebot/storage/redis_storage.py b/telebot/storage/redis_storage.py new file mode 100644 index 0000000..ff21b6e --- /dev/null +++ b/telebot/storage/redis_storage.py @@ -0,0 +1,180 @@ +from pyclbr import Class +from telebot.storage.base_storage import StateStorageBase, StateContext +import json + +redis_installed = True +try: + from redis import Redis, ConnectionPool + +except: + redis_installed = False + +class StateRedisStorage(StateStorageBase): + """ + This class is for Redis storage. + This will work only for states. + To use it, just pass this class to: + TeleBot(storage=StateRedisStorage()) + """ + def __init__(self, host='localhost', port=6379, db=0, password=None, prefix='telebot_'): + self.redis = ConnectionPool(host=host, port=port, db=db, password=password) + #self.con = Redis(connection_pool=self.redis) -> use this when necessary + # + # {chat_id: {user_id: {'state': None, 'data': {}}, ...}, ...} + self.prefix = prefix + if not redis_installed: + raise Exception("Redis is not installed. Install it via 'pip install redis'") + + def get_record(self, key): + """ + Function to get record from database. + It has nothing to do with states. + Made for backend compatibility + """ + connection = Redis(connection_pool=self.redis) + result = connection.get(self.prefix+str(key)) + connection.close() + if result: return json.loads(result) + return + + def set_record(self, key, value): + """ + Function to set record to database. + It has nothing to do with states. + Made for backend compatibility + """ + connection = Redis(connection_pool=self.redis) + connection.set(self.prefix+str(key), json.dumps(value)) + connection.close() + return True + + def delete_record(self, key): + """ + Function to delete record from database. + It has nothing to do with states. + Made for backend compatibility + """ + connection = Redis(connection_pool=self.redis) + connection.delete(self.prefix+str(key)) + connection.close() + return True + + def set_state(self, chat_id, user_id, state): + """ + Set state for a particular user in a chat. + """ + response = self.get_record(chat_id) + user_id = str(user_id) + if isinstance(state, object): + state = state.name + + if response: + if user_id in response: + response[user_id]['state'] = state + else: + response[user_id] = {'state': state, 'data': {}} + else: + response = {user_id: {'state': state, 'data': {}}} + self.set_record(chat_id, response) + + return True + + def delete_state(self, chat_id, user_id): + """ + Delete state for a particular user in a chat. + """ + response = self.get_record(chat_id) + user_id = str(user_id) + if response: + if user_id in response: + del response[user_id] + if user_id == str(chat_id): + self.delete_record(chat_id) + return True + else: self.set_record(chat_id, response) + return True + return False + + + def get_value(self, chat_id, user_id, key): + """ + Get value for a data of a user in a chat. + """ + response = self.get_record(chat_id) + user_id = str(user_id) + if response: + if user_id in response: + if key in response[user_id]['data']: + return response[user_id]['data'][key] + return None + + + def get_state(self, chat_id, user_id): + """ + Get state of a user in a chat. + """ + response = self.get_record(chat_id) + user_id = str(user_id) + if response: + if user_id in response: + return response[user_id]['state'] + + return None + + + def get_data(self, chat_id, user_id): + """ + Get data of particular user in a particular chat. + """ + response = self.get_record(chat_id) + user_id = str(user_id) + if response: + if user_id in response: + return response[user_id]['data'] + return None + + + def reset_data(self, chat_id, user_id): + """ + Reset data of a user in a chat. + """ + response = self.get_record(chat_id) + user_id = str(user_id) + if response: + if user_id in response: + response[user_id]['data'] = {} + self.set_record(chat_id, response) + return True + + + + + def set_data(self, chat_id, user_id, key, value): + """ + Set data without interactive data. + """ + response = self.get_record(chat_id) + user_id = str(user_id) + if response: + if user_id in response: + response[user_id]['data'][key] = value + self.set_record(chat_id, response) + return True + return False + + def get_interactive_data(self, chat_id, user_id): + """ + Get Data in interactive way. + You can use with() with this function. + """ + return StateContext(self, chat_id, user_id) + + def save(self, chat_id, user_id, data): + response = self.get_record(chat_id) + user_id = str(user_id) + if response: + if user_id in response: + response[user_id]['data'] = dict(data, **response[user_id]['data']) + self.set_record(chat_id, response) + return True + \ No newline at end of file diff --git a/telebot/types.py b/telebot/types.py new file mode 100644 index 0000000..081c0ed --- /dev/null +++ b/telebot/types.py @@ -0,0 +1,2888 @@ +# -*- coding: utf-8 -*- + +import logging +from typing import Dict, List, Optional, Union +from abc import ABC + +try: + import ujson as json +except ImportError: + import json + +from telebot import util + +DISABLE_KEYLEN_ERROR = False + +logger = logging.getLogger('TeleBot') + + +class JsonSerializable(object): + """ + Subclasses of this class are guaranteed to be able to be converted to JSON format. + All subclasses of this class must override to_json. + """ + + def to_json(self): + """ + Returns a JSON string representation of this class. + + This function must be overridden by subclasses. + :return: a JSON formatted string. + """ + raise NotImplementedError + + +class Dictionaryable(object): + """ + Subclasses of this class are guaranteed to be able to be converted to dictionary. + All subclasses of this class must override to_dict. + """ + + def to_dict(self): + """ + Returns a DICT with class field values + + This function must be overridden by subclasses. + :return: a DICT + """ + raise NotImplementedError + + +class JsonDeserializable(object): + """ + Subclasses of this class are guaranteed to be able to be created from a json-style dict or json formatted string. + All subclasses of this class must override de_json. + """ + + @classmethod + def de_json(cls, json_string): + """ + Returns an instance of this class from the given json dict or string. + + This function must be overridden by subclasses. + :return: an instance of this class created from the given json dict or string. + """ + raise NotImplementedError + + @staticmethod + def check_json(json_type, dict_copy = True): + """ + Checks whether json_type is a dict or a string. If it is already a dict, it is returned as-is. + If it is not, it is converted to a dict by means of json.loads(json_type) + :param json_type: input json or parsed dict + :param dict_copy: if dict is passed and it is changed outside - should be True! + :return: Dictionary parsed from json or original dict + """ + if util.is_dict(json_type): + return json_type.copy() if dict_copy else json_type + elif util.is_string(json_type): + return json.loads(json_type) + else: + raise ValueError("json_type should be a json dict or string.") + + def __str__(self): + d = { + x: y.__dict__ if hasattr(y, '__dict__') else y + for x, y in self.__dict__.items() + } + return str(d) + + +class Update(JsonDeserializable): + @classmethod + def de_json(cls, json_string): + if json_string is None: return None + obj = cls.check_json(json_string, dict_copy=False) + update_id = obj['update_id'] + message = Message.de_json(obj.get('message')) + edited_message = Message.de_json(obj.get('edited_message')) + channel_post = Message.de_json(obj.get('channel_post')) + edited_channel_post = Message.de_json(obj.get('edited_channel_post')) + inline_query = InlineQuery.de_json(obj.get('inline_query')) + chosen_inline_result = ChosenInlineResult.de_json(obj.get('chosen_inline_result')) + callback_query = CallbackQuery.de_json(obj.get('callback_query')) + shipping_query = ShippingQuery.de_json(obj.get('shipping_query')) + pre_checkout_query = PreCheckoutQuery.de_json(obj.get('pre_checkout_query')) + poll = Poll.de_json(obj.get('poll')) + poll_answer = PollAnswer.de_json(obj.get('poll_answer')) + my_chat_member = ChatMemberUpdated.de_json(obj.get('my_chat_member')) + chat_member = ChatMemberUpdated.de_json(obj.get('chat_member')) + chat_join_request = ChatJoinRequest.de_json(obj.get('chat_join_request')) + return cls(update_id, message, edited_message, channel_post, edited_channel_post, inline_query, + chosen_inline_result, callback_query, shipping_query, pre_checkout_query, poll, poll_answer, + my_chat_member, chat_member, chat_join_request) + + def __init__(self, update_id, message, edited_message, channel_post, edited_channel_post, inline_query, + chosen_inline_result, callback_query, shipping_query, pre_checkout_query, poll, poll_answer, + my_chat_member, chat_member, chat_join_request): + self.update_id = update_id + self.message = message + self.edited_message = edited_message + self.channel_post = channel_post + self.edited_channel_post = edited_channel_post + self.inline_query = inline_query + self.chosen_inline_result = chosen_inline_result + self.callback_query = callback_query + self.shipping_query = shipping_query + self.pre_checkout_query = pre_checkout_query + self.poll = poll + self.poll_answer = poll_answer + self.my_chat_member = my_chat_member + self.chat_member = chat_member + self.chat_join_request = chat_join_request + + +class ChatMemberUpdated(JsonDeserializable): + @classmethod + def de_json(cls, json_string): + if json_string is None: return None + obj = cls.check_json(json_string) + obj['chat'] = Chat.de_json(obj['chat']) + obj['from_user'] = User.de_json(obj.pop('from')) + obj['old_chat_member'] = ChatMember.de_json(obj['old_chat_member']) + obj['new_chat_member'] = ChatMember.de_json(obj['new_chat_member']) + obj['invite_link'] = ChatInviteLink.de_json(obj.get('invite_link')) + return cls(**obj) + + def __init__(self, chat, from_user, date, old_chat_member, new_chat_member, invite_link=None, **kwargs): + self.chat: Chat = chat + self.from_user: User = from_user + self.date: int = date + self.old_chat_member: ChatMember = old_chat_member + self.new_chat_member: ChatMember = new_chat_member + self.invite_link: Optional[ChatInviteLink] = invite_link + + @property + def difference(self) -> Dict[str, List]: + """ + Get the difference between `old_chat_member` and `new_chat_member` + as a dict in the following format {'parameter': [old_value, new_value]} + E.g {'status': ['member', 'kicked'], 'until_date': [None, 1625055092]} + """ + old: Dict = self.old_chat_member.__dict__ + new: Dict = self.new_chat_member.__dict__ + dif = {} + for key in new: + if key == 'user': continue + if new[key] != old[key]: + dif[key] = [old[key], new[key]] + return dif + +class ChatJoinRequest(JsonDeserializable): + @classmethod + def de_json(cls, json_string): + if json_string is None: return None + obj = cls.check_json(json_string) + obj['chat'] = Chat.de_json(obj['chat']) + obj['from_user'] = User.de_json(obj['from']) + obj['invite_link'] = ChatInviteLink.de_json(obj.get('invite_link')) + return cls(**obj) + + def __init__(self, chat, from_user, date, bio=None, invite_link=None, **kwargs): + self.chat = chat + self.from_user = from_user + self.date = date + self.bio = bio + self.invite_link = invite_link + +class WebhookInfo(JsonDeserializable): + @classmethod + def de_json(cls, json_string): + if json_string is None: return None + obj = cls.check_json(json_string, dict_copy=False) + return cls(**obj) + + def __init__(self, url, has_custom_certificate, pending_update_count, ip_address=None, + last_error_date=None, last_error_message=None, max_connections=None, + allowed_updates=None, **kwargs): + self.url = url + self.has_custom_certificate = has_custom_certificate + self.pending_update_count = pending_update_count + self.ip_address = ip_address + self.last_error_date = last_error_date + self.last_error_message = last_error_message + self.max_connections = max_connections + self.allowed_updates = allowed_updates + + +class User(JsonDeserializable, Dictionaryable, JsonSerializable): + @classmethod + def de_json(cls, json_string): + if json_string is None: return None + obj = cls.check_json(json_string, dict_copy=False) + return cls(**obj) + + def __init__(self, id, is_bot, first_name, last_name=None, username=None, language_code=None, + can_join_groups=None, can_read_all_group_messages=None, supports_inline_queries=None, **kwargs): + self.id: int = id + self.is_bot: bool = is_bot + self.first_name: str = first_name + self.username: str = username + self.last_name: str = last_name + self.language_code: str = language_code + self.can_join_groups: bool = can_join_groups + self.can_read_all_group_messages: bool = can_read_all_group_messages + self.supports_inline_queries: bool = supports_inline_queries + + @property + def full_name(self): + full_name = self.first_name + if self.last_name: + full_name += ' {0}'.format(self.last_name) + return full_name + + def to_json(self): + return json.dumps(self.to_dict()) + + def to_dict(self): + return {'id': self.id, + 'is_bot': self.is_bot, + 'first_name': self.first_name, + 'last_name': self.last_name, + 'username': self.username, + 'language_code': self.language_code, + 'can_join_groups': self.can_join_groups, + 'can_read_all_group_messages': self.can_read_all_group_messages, + 'supports_inline_queries': self.supports_inline_queries} + + +class GroupChat(JsonDeserializable): + @classmethod + def de_json(cls, json_string): + if json_string is None: return None + obj = cls.check_json(json_string, dict_copy=False) + return cls(**obj) + + def __init__(self, id, title, **kwargs): + self.id: int = id + self.title: str = title + + +class Chat(JsonDeserializable): + @classmethod + def de_json(cls, json_string): + if json_string is None: return None + obj = cls.check_json(json_string) + if 'photo' in obj: + obj['photo'] = ChatPhoto.de_json(obj['photo']) + if 'pinned_message' in obj: + obj['pinned_message'] = Message.de_json(obj['pinned_message']) + if 'permissions' in obj: + obj['permissions'] = ChatPermissions.de_json(obj['permissions']) + if 'location' in obj: + obj['location'] = ChatLocation.de_json(obj['location']) + return cls(**obj) + + def __init__(self, id, type, title=None, username=None, first_name=None, + last_name=None, photo=None, bio=None, has_private_forwards=None, + description=None, invite_link=None, pinned_message=None, + permissions=None, slow_mode_delay=None, + message_auto_delete_time=None, has_protected_content=None, sticker_set_name=None, + can_set_sticker_set=None, linked_chat_id=None, location=None, **kwargs): + self.id: int = id + self.type: str = type + self.title: str = title + self.username: str = username + self.first_name: str = first_name + self.last_name: str = last_name + self.photo: ChatPhoto = photo + self.bio: str = bio + self.has_private_forwards: bool = has_private_forwards + self.description: str = description + self.invite_link: str = invite_link + self.pinned_message: Message = pinned_message + self.permissions: ChatPermissions = permissions + self.slow_mode_delay: int = slow_mode_delay + self.message_auto_delete_time: int = message_auto_delete_time + self.has_protected_content: bool = has_protected_content + self.sticker_set_name: str = sticker_set_name + self.can_set_sticker_set: bool = can_set_sticker_set + self.linked_chat_id: int = linked_chat_id + self.location: ChatLocation = location + + +class MessageID(JsonDeserializable): + @classmethod + def de_json(cls, json_string): + if json_string is None: return None + obj = cls.check_json(json_string, dict_copy=False) + return cls(**obj) + + def __init__(self, message_id, **kwargs): + self.message_id = message_id + + +class Message(JsonDeserializable): + @classmethod + def de_json(cls, json_string): + if json_string is None: return None + obj = cls.check_json(json_string, dict_copy=False) + message_id = obj['message_id'] + from_user = User.de_json(obj.get('from')) + date = obj['date'] + chat = Chat.de_json(obj['chat']) + content_type = None + opts = {} + if 'sender_chat' in obj: + opts['sender_chat'] = Chat.de_json(obj['sender_chat']) + if 'forward_from' in obj: + opts['forward_from'] = User.de_json(obj['forward_from']) + if 'forward_from_chat' in obj: + opts['forward_from_chat'] = Chat.de_json(obj['forward_from_chat']) + if 'forward_from_message_id' in obj: + opts['forward_from_message_id'] = obj.get('forward_from_message_id') + if 'forward_signature' in obj: + opts['forward_signature'] = obj.get('forward_signature') + if 'forward_sender_name' in obj: + opts['forward_sender_name'] = obj.get('forward_sender_name') + if 'forward_date' in obj: + opts['forward_date'] = obj.get('forward_date') + if 'is_automatic_forward' in obj: + opts['is_automatic_forward'] = obj.get('is_automatic_forward') + if 'reply_to_message' in obj: + opts['reply_to_message'] = Message.de_json(obj['reply_to_message']) + if 'via_bot' in obj: + opts['via_bot'] = User.de_json(obj['via_bot']) + if 'edit_date' in obj: + opts['edit_date'] = obj.get('edit_date') + if 'has_protected_content' in obj: + opts['has_protected_content'] = obj.get('has_protected_content') + if 'media_group_id' in obj: + opts['media_group_id'] = obj.get('media_group_id') + if 'author_signature' in obj: + opts['author_signature'] = obj.get('author_signature') + if 'text' in obj: + opts['text'] = obj['text'] + content_type = 'text' + if 'entities' in obj: + opts['entities'] = Message.parse_entities(obj['entities']) + if 'caption_entities' in obj: + opts['caption_entities'] = Message.parse_entities(obj['caption_entities']) + if 'audio' in obj: + opts['audio'] = Audio.de_json(obj['audio']) + content_type = 'audio' + if 'document' in obj: + opts['document'] = Document.de_json(obj['document']) + content_type = 'document' + if 'animation' in obj: + # Document content type accompanies "animation", + # so "animation" should be checked below "document" to override it + opts['animation'] = Animation.de_json(obj['animation']) + content_type = 'animation' + if 'game' in obj: + opts['game'] = Game.de_json(obj['game']) + content_type = 'game' + if 'photo' in obj: + opts['photo'] = Message.parse_photo(obj['photo']) + content_type = 'photo' + if 'sticker' in obj: + opts['sticker'] = Sticker.de_json(obj['sticker']) + content_type = 'sticker' + if 'video' in obj: + opts['video'] = Video.de_json(obj['video']) + content_type = 'video' + if 'video_note' in obj: + opts['video_note'] = VideoNote.de_json(obj['video_note']) + content_type = 'video_note' + if 'voice' in obj: + opts['voice'] = Audio.de_json(obj['voice']) + content_type = 'voice' + if 'caption' in obj: + opts['caption'] = obj['caption'] + if 'contact' in obj: + opts['contact'] = Contact.de_json(json.dumps(obj['contact'])) + content_type = 'contact' + if 'location' in obj: + opts['location'] = Location.de_json(obj['location']) + content_type = 'location' + if 'venue' in obj: + opts['venue'] = Venue.de_json(obj['venue']) + content_type = 'venue' + if 'dice' in obj: + opts['dice'] = Dice.de_json(obj['dice']) + content_type = 'dice' + if 'new_chat_members' in obj: + new_chat_members = [] + for member in obj['new_chat_members']: + new_chat_members.append(User.de_json(member)) + opts['new_chat_members'] = new_chat_members + content_type = 'new_chat_members' + if 'left_chat_member' in obj: + opts['left_chat_member'] = User.de_json(obj['left_chat_member']) + content_type = 'left_chat_member' + if 'new_chat_title' in obj: + opts['new_chat_title'] = obj['new_chat_title'] + content_type = 'new_chat_title' + if 'new_chat_photo' in obj: + opts['new_chat_photo'] = Message.parse_photo(obj['new_chat_photo']) + content_type = 'new_chat_photo' + if 'delete_chat_photo' in obj: + opts['delete_chat_photo'] = obj['delete_chat_photo'] + content_type = 'delete_chat_photo' + if 'group_chat_created' in obj: + opts['group_chat_created'] = obj['group_chat_created'] + content_type = 'group_chat_created' + if 'supergroup_chat_created' in obj: + opts['supergroup_chat_created'] = obj['supergroup_chat_created'] + content_type = 'supergroup_chat_created' + if 'channel_chat_created' in obj: + opts['channel_chat_created'] = obj['channel_chat_created'] + content_type = 'channel_chat_created' + if 'migrate_to_chat_id' in obj: + opts['migrate_to_chat_id'] = obj['migrate_to_chat_id'] + content_type = 'migrate_to_chat_id' + if 'migrate_from_chat_id' in obj: + opts['migrate_from_chat_id'] = obj['migrate_from_chat_id'] + content_type = 'migrate_from_chat_id' + if 'pinned_message' in obj: + opts['pinned_message'] = Message.de_json(obj['pinned_message']) + content_type = 'pinned_message' + if 'invoice' in obj: + opts['invoice'] = Invoice.de_json(obj['invoice']) + content_type = 'invoice' + if 'successful_payment' in obj: + opts['successful_payment'] = SuccessfulPayment.de_json(obj['successful_payment']) + content_type = 'successful_payment' + if 'connected_website' in obj: + opts['connected_website'] = obj['connected_website'] + content_type = 'connected_website' + if 'poll' in obj: + opts['poll'] = Poll.de_json(obj['poll']) + content_type = 'poll' + if 'passport_data' in obj: + opts['passport_data'] = obj['passport_data'] + content_type = 'passport_data' + if 'proximity_alert_triggered' in obj: + opts['proximity_alert_triggered'] = ProximityAlertTriggered.de_json(obj[ + 'proximity_alert_triggered']) + content_type = 'proximity_alert_triggered' + if 'voice_chat_scheduled' in obj: + opts['voice_chat_scheduled'] = VoiceChatScheduled.de_json(obj['voice_chat_scheduled']) + content_type = 'voice_chat_scheduled' + if 'voice_chat_started' in obj: + opts['voice_chat_started'] = VoiceChatStarted.de_json(obj['voice_chat_started']) + content_type = 'voice_chat_started' + if 'voice_chat_ended' in obj: + opts['voice_chat_ended'] = VoiceChatEnded.de_json(obj['voice_chat_ended']) + content_type = 'voice_chat_ended' + if 'voice_chat_participants_invited' in obj: + opts['voice_chat_participants_invited'] = VoiceChatParticipantsInvited.de_json(obj['voice_chat_participants_invited']) + content_type = 'voice_chat_participants_invited' + if 'message_auto_delete_timer_changed' in obj: + opts['message_auto_delete_timer_changed'] = MessageAutoDeleteTimerChanged.de_json(obj['message_auto_delete_timer_changed']) + content_type = 'message_auto_delete_timer_changed' + if 'reply_markup' in obj: + opts['reply_markup'] = InlineKeyboardMarkup.de_json(obj['reply_markup']) + return cls(message_id, from_user, date, chat, content_type, opts, json_string) + + @classmethod + def parse_chat(cls, chat): + if 'first_name' not in chat: + return GroupChat.de_json(chat) + else: + return User.de_json(chat) + + @classmethod + def parse_photo(cls, photo_size_array): + ret = [] + for ps in photo_size_array: + ret.append(PhotoSize.de_json(ps)) + return ret + + @classmethod + def parse_entities(cls, message_entity_array): + ret = [] + for me in message_entity_array: + ret.append(MessageEntity.de_json(me)) + return ret + + def __init__(self, message_id, from_user, date, chat, content_type, options, json_string): + self.content_type: str = content_type + self.id: int = message_id # Lets fix the telegram usability ####up with ID in Message :) + self.message_id: int = message_id + self.from_user: User = from_user + self.date: int = date + self.chat: Chat = chat + self.sender_chat: Optional[Chat] = None + self.forward_from: Optional[User] = None + self.forward_from_chat: Optional[Chat] = None + self.forward_from_message_id: Optional[int] = None + self.forward_signature: Optional[str] = None + self.forward_sender_name: Optional[str] = None + self.forward_date: Optional[int] = None + self.is_automatic_forward: Optional[bool] = None + self.reply_to_message: Optional[Message] = None + self.via_bot: Optional[User] = None + self.edit_date: Optional[int] = None + self.has_protected_content: Optional[bool] = None + self.media_group_id: Optional[str] = None + self.author_signature: Optional[str] = None + self.text: Optional[str] = None + self.entities: Optional[List[MessageEntity]] = None + self.caption_entities: Optional[List[MessageEntity]] = None + self.audio: Optional[Audio] = None + self.document: Optional[Document] = None + self.photo: Optional[List[PhotoSize]] = None + self.sticker: Optional[Sticker] = None + self.video: Optional[Video] = None + self.video_note: Optional[VideoNote] = None + self.voice: Optional[Voice] = None + self.caption: Optional[str] = None + self.contact: Optional[Contact] = None + self.location: Optional[Location] = None + self.venue: Optional[Venue] = None + self.animation: Optional[Animation] = None + self.dice: Optional[Dice] = None + self.new_chat_member: Optional[User] = None # Deprecated since Bot API 3.0. Not processed anymore + self.new_chat_members: Optional[List[User]] = None + self.left_chat_member: Optional[User] = None + self.new_chat_title: Optional[str] = None + self.new_chat_photo: Optional[List[PhotoSize]] = None + self.delete_chat_photo: Optional[bool] = None + self.group_chat_created: Optional[bool] = None + self.supergroup_chat_created: Optional[bool] = None + self.channel_chat_created: Optional[bool] = None + self.migrate_to_chat_id: Optional[int] = None + self.migrate_from_chat_id: Optional[int] = None + self.pinned_message: Optional[Message] = None + self.invoice: Optional[Invoice] = None + self.successful_payment: Optional[SuccessfulPayment] = None + self.connected_website: Optional[str] = None + self.reply_markup: Optional[InlineKeyboardMarkup] = None + for key in options: + setattr(self, key, options[key]) + self.json = json_string + + def __html_text(self, text, entities): + """ + Author: @sviat9440 + Updaters: @badiboy + Message: "*Test* parse _formatting_, [url](https://example.com), [text_mention](tg://user?id=123456) and mention @username" + + Example: + message.html_text + >> "Test parse formatting, url, text_mention and mention @username" + + Custom subs: + You can customize the substitutes. By default, there is no substitute for the entities: hashtag, bot_command, email. You can add or modify substitute an existing entity. + Example: + message.custom_subs = {"bold": "{text}", "italic": "{text}", "mention": "{text}"} + message.html_text + >> "Test parse formatting, url and text_mention and mention @username" + """ + + if not entities: + return text + + _subs = { + "bold": "{text}", + "italic": "{text}", + "pre": "
{text}
", + "code": "{text}", + # "url": "{text}", # @badiboy plain URLs have no text and do not need tags + "text_link": "{text}", + "strikethrough": "{text}", + "underline": "{text}", + "spoiler": "{text}", + } + + if hasattr(self, "custom_subs"): + for key, value in self.custom_subs.items(): + _subs[key] = value + utf16_text = text.encode("utf-16-le") + html_text = "" + + def func(upd_text, subst_type=None, url=None, user=None): + upd_text = upd_text.decode("utf-16-le") + if subst_type == "text_mention": + subst_type = "text_link" + url = "tg://user?id={0}".format(user.id) + elif subst_type == "mention": + url = "https://t.me/{0}".format(upd_text[1:]) + upd_text = upd_text.replace("&", "&").replace("<", "<").replace(">", ">") + if not subst_type or not _subs.get(subst_type): + return upd_text + subs = _subs.get(subst_type) + return subs.format(text=upd_text, url=url) + + offset = 0 + for entity in entities: + if entity.offset > offset: + html_text += func(utf16_text[offset * 2 : entity.offset * 2]) + offset = entity.offset + html_text += func(utf16_text[offset * 2 : (offset + entity.length) * 2], entity.type, entity.url, entity.user) + offset += entity.length + elif entity.offset == offset: + html_text += func(utf16_text[offset * 2 : (offset + entity.length) * 2], entity.type, entity.url, entity.user) + offset += entity.length + else: + # TODO: process nested entities from Bot API 4.5 + # Now ignoring them + pass + if offset * 2 < len(utf16_text): + html_text += func(utf16_text[offset * 2:]) + return html_text + + @property + def html_text(self): + return self.__html_text(self.text, self.entities) + + @property + def html_caption(self): + return self.__html_text(self.caption, self.caption_entities) + + +class MessageEntity(Dictionaryable, JsonSerializable, JsonDeserializable): + @staticmethod + def to_list_of_dicts(entity_list) -> Union[List[Dict], None]: + res = [] + for e in entity_list: + res.append(MessageEntity.to_dict(e)) + return res or None + + @classmethod + def de_json(cls, json_string): + if json_string is None: return None + obj = cls.check_json(json_string) + if 'user' in obj: + obj['user'] = User.de_json(obj['user']) + return cls(**obj) + + def __init__(self, type, offset, length, url=None, user=None, language=None, **kwargs): + self.type: str = type + self.offset: int = offset + self.length: int = length + self.url: str = url + self.user: User = user + self.language: str = language + + def to_json(self): + return json.dumps(self.to_dict()) + + def to_dict(self): + return {"type": self.type, + "offset": self.offset, + "length": self.length, + "url": self.url, + "user": self.user, + "language": self.language} + + +class Dice(JsonSerializable, Dictionaryable, JsonDeserializable): + @classmethod + def de_json(cls, json_string): + if json_string is None: return None + obj = cls.check_json(json_string, dict_copy=False) + return cls(**obj) + + def __init__(self, value, emoji, **kwargs): + self.value: int = value + self.emoji: str = emoji + + def to_json(self): + return json.dumps(self.to_dict()) + + def to_dict(self): + return {'value': self.value, + 'emoji': self.emoji} + + +class PhotoSize(JsonDeserializable): + @classmethod + def de_json(cls, json_string): + if json_string is None: return None + obj = cls.check_json(json_string, dict_copy=False) + return cls(**obj) + + def __init__(self, file_id, file_unique_id, width, height, file_size=None, **kwargs): + self.file_id: str = file_id + self.file_unique_id: str = file_unique_id + self.width: int = width + self.height: int = height + self.file_size: int = file_size + + +class Audio(JsonDeserializable): + @classmethod + def de_json(cls, json_string): + if json_string is None: return None + obj = cls.check_json(json_string) + if 'thumb' in obj and 'file_id' in obj['thumb']: + obj['thumb'] = PhotoSize.de_json(obj['thumb']) + else: + obj['thumb'] = None + return cls(**obj) + + def __init__(self, file_id, file_unique_id, duration, performer=None, title=None, file_name=None, mime_type=None, + file_size=None, thumb=None, **kwargs): + self.file_id: str = file_id + self.file_unique_id: str = file_unique_id + self.duration: int = duration + self.performer: str = performer + self.title: str = title + self.file_name: str = file_name + self.mime_type: str = mime_type + self.file_size: int = file_size + self.thumb: PhotoSize = thumb + + +class Voice(JsonDeserializable): + @classmethod + def de_json(cls, json_string): + if json_string is None: return None + obj = cls.check_json(json_string, dict_copy=False) + return cls(**obj) + + def __init__(self, file_id, file_unique_id, duration, mime_type=None, file_size=None, **kwargs): + self.file_id: str = file_id + self.file_unique_id: str = file_unique_id + self.duration: int = duration + self.mime_type: str = mime_type + self.file_size: int = file_size + + +class Document(JsonDeserializable): + @classmethod + def de_json(cls, json_string): + if json_string is None: return None + obj = cls.check_json(json_string) + if 'thumb' in obj and 'file_id' in obj['thumb']: + obj['thumb'] = PhotoSize.de_json(obj['thumb']) + else: + obj['thumb'] = None + return cls(**obj) + + def __init__(self, file_id, file_unique_id, thumb=None, file_name=None, mime_type=None, file_size=None, **kwargs): + self.file_id: str = file_id + self.file_unique_id: str = file_unique_id + self.thumb: PhotoSize = thumb + self.file_name: str = file_name + self.mime_type: str = mime_type + self.file_size: int = file_size + + +class Video(JsonDeserializable): + @classmethod + def de_json(cls, json_string): + if json_string is None: return None + obj = cls.check_json(json_string) + if 'thumb' in obj and 'file_id' in obj['thumb']: + obj['thumb'] = PhotoSize.de_json(obj['thumb']) + return cls(**obj) + + def __init__(self, file_id, file_unique_id, width, height, duration, thumb=None, file_name=None, mime_type=None, file_size=None, **kwargs): + self.file_id: str = file_id + self.file_unique_id: str = file_unique_id + self.width: int = width + self.height: int = height + self.duration: int = duration + self.thumb: PhotoSize = thumb + self.file_name: str = file_name + self.mime_type: str = mime_type + self.file_size: int = file_size + + +class VideoNote(JsonDeserializable): + @classmethod + def de_json(cls, json_string): + if json_string is None: return None + obj = cls.check_json(json_string) + if 'thumb' in obj and 'file_id' in obj['thumb']: + obj['thumb'] = PhotoSize.de_json(obj['thumb']) + return cls(**obj) + + def __init__(self, file_id, file_unique_id, length, duration, thumb=None, file_size=None, **kwargs): + self.file_id: str = file_id + self.file_unique_id: str = file_unique_id + self.length: int = length + self.duration: int = duration + self.thumb: PhotoSize = thumb + self.file_size: int = file_size + + +class Contact(JsonDeserializable): + @classmethod + def de_json(cls, json_string): + if json_string is None: return None + obj = cls.check_json(json_string, dict_copy=False) + return cls(**obj) + + def __init__(self, phone_number, first_name, last_name=None, user_id=None, vcard=None, **kwargs): + self.phone_number: str = phone_number + self.first_name: str = first_name + self.last_name: str = last_name + self.user_id: int = user_id + self.vcard: str = vcard + + +class Location(JsonDeserializable, JsonSerializable, Dictionaryable): + @classmethod + def de_json(cls, json_string): + if json_string is None: return None + obj = cls.check_json(json_string, dict_copy=False) + return cls(**obj) + + def __init__(self, longitude, latitude, horizontal_accuracy=None, + live_period=None, heading=None, proximity_alert_radius=None, **kwargs): + self.longitude: float = longitude + self.latitude: float = latitude + self.horizontal_accuracy: float = horizontal_accuracy + self.live_period: int = live_period + self.heading: int = heading + self.proximity_alert_radius: int = proximity_alert_radius + + def to_json(self): + return json.dumps(self.to_dict()) + + def to_dict(self): + return { + "longitude": self.longitude, + "latitude": self.latitude, + "horizontal_accuracy": self.horizontal_accuracy, + "live_period": self.live_period, + "heading": self.heading, + "proximity_alert_radius": self.proximity_alert_radius, + } + + +class Venue(JsonDeserializable): + @classmethod + def de_json(cls, json_string): + if json_string is None: return None + obj = cls.check_json(json_string) + obj['location'] = Location.de_json(obj['location']) + return cls(**obj) + + def __init__(self, location, title, address, foursquare_id=None, foursquare_type=None, + google_place_id=None, google_place_type=None, **kwargs): + self.location: Location = location + self.title: str = title + self.address: str = address + self.foursquare_id: str = foursquare_id + self.foursquare_type: str = foursquare_type + self.google_place_id: str = google_place_id + self.google_place_type: str = google_place_type + + +class UserProfilePhotos(JsonDeserializable): + @classmethod + def de_json(cls, json_string): + if json_string is None: return None + obj = cls.check_json(json_string) + if 'photos' in obj: + photos = [[PhotoSize.de_json(y) for y in x] for x in obj['photos']] + obj['photos'] = photos + return cls(**obj) + + def __init__(self, total_count, photos=None, **kwargs): + self.total_count: int = total_count + self.photos: List[PhotoSize] = photos + + +class File(JsonDeserializable): + @classmethod + def de_json(cls, json_string): + if json_string is None: return None + obj = cls.check_json(json_string, dict_copy=False) + return cls(**obj) + + def __init__(self, file_id, file_unique_id, file_size, file_path, **kwargs): + self.file_id: str = file_id + self.file_unique_id: str = file_unique_id + self.file_size: int = file_size + self.file_path: str = file_path + + +class ForceReply(JsonSerializable): + def __init__(self, selective: Optional[bool]=None, input_field_placeholder: Optional[str]=None): + self.selective: bool = selective + self.input_field_placeholder: str = input_field_placeholder + + def to_json(self): + json_dict = {'force_reply': True} + if self.selective is not None: + json_dict['selective'] = self.selective + if self.input_field_placeholder: + json_dict['input_field_placeholder'] = self.input_field_placeholder + return json.dumps(json_dict) + + +class ReplyKeyboardRemove(JsonSerializable): + def __init__(self, selective=None): + self.selective: bool = selective + + def to_json(self): + json_dict = {'remove_keyboard': True} + if self.selective: + json_dict['selective'] = self.selective + return json.dumps(json_dict) + + +class ReplyKeyboardMarkup(JsonSerializable): + max_row_keys = 12 + + def __init__(self, resize_keyboard: Optional[bool]=None, one_time_keyboard: Optional[bool]=None, + selective: Optional[bool]=None, row_width: int=3, input_field_placeholder: Optional[str]=None): + if row_width > self.max_row_keys: + # Todo: Will be replaced with Exception in future releases + if not DISABLE_KEYLEN_ERROR: + logger.error('Telegram does not support reply keyboard row width over %d.' % self.max_row_keys) + row_width = self.max_row_keys + + self.resize_keyboard: bool = resize_keyboard + self.one_time_keyboard: bool = one_time_keyboard + self.selective: bool = selective + self.row_width: int = row_width + self.input_field_placeholder: str = input_field_placeholder + self.keyboard: List[List[KeyboardButton]] = [] + + def add(self, *args, row_width=None): + """ + This function adds strings to the keyboard, while not exceeding row_width. + E.g. ReplyKeyboardMarkup#add("A", "B", "C") yields the json result {keyboard: [["A"], ["B"], ["C"]]} + when row_width is set to 1. + When row_width is set to 2, the following is the result of this function: {keyboard: [["A", "B"], ["C"]]} + See https://core.telegram.org/bots/api#replykeyboardmarkup + :param args: KeyboardButton to append to the keyboard + :param row_width: width of row + :return: self, to allow function chaining. + """ + if row_width is None: + row_width = self.row_width + + if row_width > self.max_row_keys: + # Todo: Will be replaced with Exception in future releases + if not DISABLE_KEYLEN_ERROR: + logger.error('Telegram does not support reply keyboard row width over %d.' % self.max_row_keys) + row_width = self.max_row_keys + + for row in util.chunks(args, row_width): + button_array = [] + for button in row: + if util.is_string(button): + button_array.append({'text': button}) + elif util.is_bytes(button): + button_array.append({'text': button.decode('utf-8')}) + else: + button_array.append(button.to_dict()) + self.keyboard.append(button_array) + + return self + + def row(self, *args): + """ + Adds a list of KeyboardButton to the keyboard. This function does not consider row_width. + ReplyKeyboardMarkup#row("A")#row("B", "C")#to_json() outputs '{keyboard: [["A"], ["B", "C"]]}' + See https://core.telegram.org/bots/api#replykeyboardmarkup + :param args: strings + :return: self, to allow function chaining. + """ + + return self.add(*args, row_width=self.max_row_keys) + + def to_json(self): + """ + Converts this object to its json representation following the Telegram API guidelines described here: + https://core.telegram.org/bots/api#replykeyboardmarkup + :return: + """ + json_dict = {'keyboard': self.keyboard} + if self.one_time_keyboard is not None: + json_dict['one_time_keyboard'] = self.one_time_keyboard + if self.resize_keyboard is not None: + json_dict['resize_keyboard'] = self.resize_keyboard + if self.selective is not None: + json_dict['selective'] = self.selective + if self.input_field_placeholder: + json_dict['input_field_placeholder'] = self.input_field_placeholder + return json.dumps(json_dict) + + +class KeyboardButtonPollType(Dictionaryable): + def __init__(self, type=''): + self.type: str = type + + def to_dict(self): + return {'type': self.type} + + +class KeyboardButton(Dictionaryable, JsonSerializable): + def __init__(self, text: str, request_contact: Optional[bool]=None, + request_location: Optional[bool]=None, request_poll: Optional[KeyboardButtonPollType]=None): + self.text: str = text + self.request_contact: bool = request_contact + self.request_location: bool = request_location + self.request_poll: KeyboardButtonPollType = request_poll + + def to_json(self): + return json.dumps(self.to_dict()) + + def to_dict(self): + json_dict = {'text': self.text} + if self.request_contact is not None: + json_dict['request_contact'] = self.request_contact + if self.request_location is not None: + json_dict['request_location'] = self.request_location + if self.request_poll is not None: + json_dict['request_poll'] = self.request_poll.to_dict() + return json_dict + + +class InlineKeyboardMarkup(Dictionaryable, JsonSerializable, JsonDeserializable): + max_row_keys = 8 + + @classmethod + def de_json(cls, json_string): + if json_string is None: return None + obj = cls.check_json(json_string, dict_copy=False) + keyboard = [[InlineKeyboardButton.de_json(button) for button in row] for row in obj['inline_keyboard']] + return cls(keyboard = keyboard) + + def __init__(self, keyboard=None, row_width=3): + """ + This object represents an inline keyboard that appears + right next to the message it belongs to. + + :return: None + """ + if row_width > self.max_row_keys: + # Todo: Will be replaced with Exception in future releases + logger.error('Telegram does not support inline keyboard row width over %d.' % self.max_row_keys) + row_width = self.max_row_keys + + self.row_width: int = row_width + self.keyboard: List[List[InlineKeyboardButton]] = keyboard or [] + + def add(self, *args, row_width=None): + """ + This method adds buttons to the keyboard without exceeding row_width. + + E.g. InlineKeyboardMarkup.add("A", "B", "C") yields the json result: + {keyboard: [["A"], ["B"], ["C"]]} + when row_width is set to 1. + When row_width is set to 2, the result: + {keyboard: [["A", "B"], ["C"]]} + See https://core.telegram.org/bots/api#inlinekeyboardmarkup + + :param args: Array of InlineKeyboardButton to append to the keyboard + :param row_width: width of row + :return: self, to allow function chaining. + """ + if row_width is None: + row_width = self.row_width + + if row_width > self.max_row_keys: + # Todo: Will be replaced with Exception in future releases + logger.error('Telegram does not support inline keyboard row width over %d.' % self.max_row_keys) + row_width = self.max_row_keys + + for row in util.chunks(args, row_width): + button_array = [button for button in row] + self.keyboard.append(button_array) + + return self + + def row(self, *args): + """ + Adds a list of InlineKeyboardButton to the keyboard. + This method does not consider row_width. + + InlineKeyboardMarkup.row("A").row("B", "C").to_json() outputs: + '{keyboard: [["A"], ["B", "C"]]}' + See https://core.telegram.org/bots/api#inlinekeyboardmarkup + + :param args: Array of InlineKeyboardButton to append to the keyboard + :return: self, to allow function chaining. + """ + + return self.add(*args, row_width=self.max_row_keys) + + def to_json(self): + """ + Converts this object to its json representation + following the Telegram API guidelines described here: + https://core.telegram.org/bots/api#inlinekeyboardmarkup + :return: + """ + return json.dumps(self.to_dict()) + + def to_dict(self): + json_dict = dict() + json_dict['inline_keyboard'] = [[button.to_dict() for button in row] for row in self.keyboard] + return json_dict + + +class InlineKeyboardButton(Dictionaryable, JsonSerializable, JsonDeserializable): + @classmethod + def de_json(cls, json_string): + if json_string is None: return None + obj = cls.check_json(json_string) + if 'login_url' in obj: + obj['login_url'] = LoginUrl.de_json(obj.get('login_url')) + return cls(**obj) + + def __init__(self, text, url=None, callback_data=None, switch_inline_query=None, + switch_inline_query_current_chat=None, callback_game=None, pay=None, login_url=None, **kwargs): + self.text: str = text + self.url: str = url + self.callback_data: str = callback_data + self.switch_inline_query: str = switch_inline_query + self.switch_inline_query_current_chat: str = switch_inline_query_current_chat + self.callback_game = callback_game # Not Implemented + self.pay: bool = pay + self.login_url: LoginUrl = login_url + + def to_json(self): + return json.dumps(self.to_dict()) + + def to_dict(self): + json_dict = {'text': self.text} + if self.url: + json_dict['url'] = self.url + if self.callback_data: + json_dict['callback_data'] = self.callback_data + if self.switch_inline_query is not None: + json_dict['switch_inline_query'] = self.switch_inline_query + if self.switch_inline_query_current_chat is not None: + json_dict['switch_inline_query_current_chat'] = self.switch_inline_query_current_chat + if self.callback_game is not None: + json_dict['callback_game'] = self.callback_game + if self.pay is not None: + json_dict['pay'] = self.pay + if self.login_url is not None: + json_dict['login_url'] = self.login_url.to_dict() + return json_dict + + +class LoginUrl(Dictionaryable, JsonSerializable, JsonDeserializable): + @classmethod + def de_json(cls, json_string): + if json_string is None: return None + obj = cls.check_json(json_string, dict_copy=False) + return cls(**obj) + + def __init__(self, url, forward_text=None, bot_username=None, request_write_access=None, **kwargs): + self.url: str = url + self.forward_text: str = forward_text + self.bot_username: str = bot_username + self.request_write_access: bool = request_write_access + + def to_json(self): + return json.dumps(self.to_dict()) + + def to_dict(self): + json_dict = {'url': self.url} + if self.forward_text: + json_dict['forward_text'] = self.forward_text + if self.bot_username: + json_dict['bot_username'] = self.bot_username + if self.request_write_access is not None: + json_dict['request_write_access'] = self.request_write_access + return json_dict + + +class CallbackQuery(JsonDeserializable): + @classmethod + def de_json(cls, json_string): + if json_string is None: return None + obj = cls.check_json(json_string) + if not "data" in obj: + # "data" field is Optional in the API, but historically is mandatory in the class constructor + obj['data'] = None + obj['from_user'] = User.de_json(obj.pop('from')) + if 'message' in obj: + obj['message'] = Message.de_json(obj.get('message')) + return cls(**obj) + + def __init__(self, id, from_user, data, chat_instance, message=None, inline_message_id=None, game_short_name=None, **kwargs): + self.id: int = id + self.from_user: User = from_user + self.message: Message = message + self.inline_message_id: str = inline_message_id + self.chat_instance: str = chat_instance + self.data: str = data + self.game_short_name: str = game_short_name + + +class ChatPhoto(JsonDeserializable): + @classmethod + def de_json(cls, json_string): + if json_string is None: return None + obj = cls.check_json(json_string, dict_copy=False) + return cls(**obj) + + def __init__(self, small_file_id, small_file_unique_id, big_file_id, big_file_unique_id, **kwargs): + self.small_file_id: str = small_file_id + self.small_file_unique_id: str = small_file_unique_id + self.big_file_id: str = big_file_id + self.big_file_unique_id: str = big_file_unique_id + + +class ChatMember(JsonDeserializable): + @classmethod + def de_json(cls, json_string): + if json_string is None: return None + obj = cls.check_json(json_string) + obj['user'] = User.de_json(obj['user']) + return cls(**obj) + + def __init__(self, user, status, custom_title=None, is_anonymous=None, can_be_edited=None, + can_post_messages=None, can_edit_messages=None, can_delete_messages=None, + can_restrict_members=None, can_promote_members=None, can_change_info=None, + can_invite_users=None, can_pin_messages=None, is_member=None, + can_send_messages=None, can_send_media_messages=None, can_send_polls=None, + can_send_other_messages=None, can_add_web_page_previews=None, + can_manage_chat=None, can_manage_voice_chats=None, + until_date=None, **kwargs): + self.user: User = user + self.status: str = status + self.custom_title: str = custom_title + self.is_anonymous: bool = is_anonymous + self.can_be_edited: bool = can_be_edited + self.can_post_messages: bool = can_post_messages + self.can_edit_messages: bool = can_edit_messages + self.can_delete_messages: bool = can_delete_messages + self.can_restrict_members: bool = can_restrict_members + self.can_promote_members: bool = can_promote_members + self.can_change_info: bool = can_change_info + self.can_invite_users: bool = can_invite_users + self.can_pin_messages: bool = can_pin_messages + self.is_member: bool = is_member + self.can_send_messages: bool = can_send_messages + self.can_send_media_messages: bool = can_send_media_messages + self.can_send_polls: bool = can_send_polls + self.can_send_other_messages: bool = can_send_other_messages + self.can_add_web_page_previews: bool = can_add_web_page_previews + self.can_manage_chat: bool = can_manage_chat + self.can_manage_voice_chats: bool = can_manage_voice_chats + self.until_date: int = until_date + + +class ChatMemberOwner(ChatMember): + pass + +class ChatMemberAdministrator(ChatMember): + pass + + +class ChatMemberMember(ChatMember): + pass + + +class ChatMemberRestricted(ChatMember): + pass + + +class ChatMemberLeft(ChatMember): + pass + + +class ChatMemberBanned(ChatMember): + pass + + +class ChatPermissions(JsonDeserializable, JsonSerializable, Dictionaryable): + @classmethod + def de_json(cls, json_string): + if json_string is None: return json_string + obj = cls.check_json(json_string, dict_copy=False) + return cls(**obj) + + def __init__(self, can_send_messages=None, can_send_media_messages=None, + can_send_polls=None, can_send_other_messages=None, + can_add_web_page_previews=None, can_change_info=None, + can_invite_users=None, can_pin_messages=None, **kwargs): + self.can_send_messages: bool = can_send_messages + self.can_send_media_messages: bool = can_send_media_messages + self.can_send_polls: bool = can_send_polls + self.can_send_other_messages: bool = can_send_other_messages + self.can_add_web_page_previews: bool = can_add_web_page_previews + self.can_change_info: bool = can_change_info + self.can_invite_users: bool = can_invite_users + self.can_pin_messages: bool = can_pin_messages + + def to_json(self): + return json.dumps(self.to_dict()) + + def to_dict(self): + json_dict = dict() + if self.can_send_messages is not None: + json_dict['can_send_messages'] = self.can_send_messages + if self.can_send_media_messages is not None: + json_dict['can_send_media_messages'] = self.can_send_media_messages + if self.can_send_polls is not None: + json_dict['can_send_polls'] = self.can_send_polls + if self.can_send_other_messages is not None: + json_dict['can_send_other_messages'] = self.can_send_other_messages + if self.can_add_web_page_previews is not None: + json_dict['can_add_web_page_previews'] = self.can_add_web_page_previews + if self.can_change_info is not None: + json_dict['can_change_info'] = self.can_change_info + if self.can_invite_users is not None: + json_dict['can_invite_users'] = self.can_invite_users + if self.can_pin_messages is not None: + json_dict['can_pin_messages'] = self.can_pin_messages + return json_dict + + +class BotCommand(JsonSerializable, JsonDeserializable): + @classmethod + def de_json(cls, json_string): + if json_string is None: return None + obj = cls.check_json(json_string, dict_copy=False) + return cls(**obj) + + def __init__(self, command, description): + """ + This object represents a bot command. + :param command: Text of the command, 1-32 characters. + Can contain only lowercase English letters, digits and underscores. + :param description: Description of the command, 3-256 characters. + :return: + """ + self.command: str = command + self.description: str = description + + def to_json(self): + return json.dumps(self.to_dict()) + + def to_dict(self): + return {'command': self.command, 'description': self.description} + + +# BotCommandScopes + +class BotCommandScope(ABC, JsonSerializable): + def __init__(self, type='default', chat_id=None, user_id=None): + """ + Abstract class. + Use BotCommandScopeX classes to set a specific scope type: + BotCommandScopeDefault + BotCommandScopeAllPrivateChats + BotCommandScopeAllGroupChats + BotCommandScopeAllChatAdministrators + BotCommandScopeChat + BotCommandScopeChatAdministrators + BotCommandScopeChatMember + """ + self.type: str = type + self.chat_id: Optional[Union[int, str]] = chat_id + self.user_id: Optional[Union[int, str]] = user_id + + def to_json(self): + json_dict = {'type': self.type} + if self.chat_id: + json_dict['chat_id'] = self.chat_id + if self.user_id: + json_dict['user_id'] = self.user_id + return json.dumps(json_dict) + + +class BotCommandScopeDefault(BotCommandScope): + def __init__(self): + """ + Represents the default scope of bot commands. + Default commands are used if no commands with a narrower scope are specified for the user. + """ + super(BotCommandScopeDefault, self).__init__(type='default') + + +class BotCommandScopeAllPrivateChats(BotCommandScope): + def __init__(self): + """ + Represents the scope of bot commands, covering all private chats. + """ + super(BotCommandScopeAllPrivateChats, self).__init__(type='all_private_chats') + + +class BotCommandScopeAllGroupChats(BotCommandScope): + def __init__(self): + """ + Represents the scope of bot commands, covering all group and supergroup chats. + """ + super(BotCommandScopeAllGroupChats, self).__init__(type='all_group_chats') + + +class BotCommandScopeAllChatAdministrators(BotCommandScope): + def __init__(self): + """ + Represents the scope of bot commands, covering all group and supergroup chat administrators. + """ + super(BotCommandScopeAllChatAdministrators, self).__init__(type='all_chat_administrators') + + +class BotCommandScopeChat(BotCommandScope): + def __init__(self, chat_id=None): + super(BotCommandScopeChat, self).__init__(type='chat', chat_id=chat_id) + + +class BotCommandScopeChatAdministrators(BotCommandScope): + def __init__(self, chat_id=None): + """ + Represents the scope of bot commands, covering a specific chat. + @param chat_id: Unique identifier for the target chat + """ + super(BotCommandScopeChatAdministrators, self).__init__(type='chat_administrators', chat_id=chat_id) + + +class BotCommandScopeChatMember(BotCommandScope): + def __init__(self, chat_id=None, user_id=None): + """ + Represents the scope of bot commands, covering all administrators of a specific group or supergroup chat + @param chat_id: Unique identifier for the target chat + @param user_id: Unique identifier of the target user + """ + super(BotCommandScopeChatMember, self).__init__(type='chat_member', chat_id=chat_id, user_id=user_id) + + +# InlineQuery + +class InlineQuery(JsonDeserializable): + @classmethod + def de_json(cls, json_string): + if json_string is None: return None + obj = cls.check_json(json_string) + obj['from_user'] = User.de_json(obj.pop('from')) + if 'location' in obj: + obj['location'] = Location.de_json(obj['location']) + return cls(**obj) + + def __init__(self, id, from_user, query, offset, chat_type=None, location=None, **kwargs): + """ + This object represents an incoming inline query. + When the user sends an empty query, your bot could + return some default or trending results. + :param id: string Unique identifier for this query + :param from_user: User Sender + :param query: String Text of the query + :param chat_type: String Type of the chat, from which the inline query was sent. + Can be either “sender” for a private chat with the inline query sender, + “private”, “group”, “supergroup”, or “channel”. + :param offset: String Offset of the results to be returned, can be controlled by the bot + :param location: Sender location, only for bots that request user location + :return: InlineQuery Object + """ + self.id: int = id + self.from_user: User = from_user + self.query: str = query + self.offset: str = offset + self.chat_type: str = chat_type + self.location: Location = location + + +class InputTextMessageContent(Dictionaryable): + def __init__(self, message_text, parse_mode=None, entities=None, disable_web_page_preview=None): + self.message_text: str = message_text + self.parse_mode: str = parse_mode + self.entities: List[MessageEntity] = entities + self.disable_web_page_preview: bool = disable_web_page_preview + + def to_dict(self): + json_dict = {'message_text': self.message_text} + if self.parse_mode: + json_dict['parse_mode'] = self.parse_mode + if self.entities: + json_dict['entities'] = MessageEntity.to_list_of_dicts(self.entities) + if self.disable_web_page_preview is not None: + json_dict['disable_web_page_preview'] = self.disable_web_page_preview + return json_dict + + +class InputLocationMessageContent(Dictionaryable): + def __init__(self, latitude, longitude, horizontal_accuracy=None, live_period=None, heading=None, proximity_alert_radius=None): + self.latitude: float = latitude + self.longitude: float = longitude + self.horizontal_accuracy: float = horizontal_accuracy + self.live_period: int = live_period + self.heading: int = heading + self.proximity_alert_radius: int = proximity_alert_radius + + def to_dict(self): + json_dict = {'latitude': self.latitude, 'longitude': self.longitude} + if self.horizontal_accuracy: + json_dict['horizontal_accuracy'] = self.horizontal_accuracy + if self.live_period: + json_dict['live_period'] = self.live_period + if self.heading: + json_dict['heading'] = self.heading + if self.proximity_alert_radius: + json_dict['proximity_alert_radius'] = self.proximity_alert_radius + return json_dict + + +class InputVenueMessageContent(Dictionaryable): + def __init__(self, latitude, longitude, title, address, foursquare_id=None, foursquare_type=None, + google_place_id=None, google_place_type=None): + self.latitude: float = latitude + self.longitude: float = longitude + self.title: str = title + self.address: str = address + self.foursquare_id: str = foursquare_id + self.foursquare_type: str = foursquare_type + self.google_place_id: str = google_place_id + self.google_place_type: str = google_place_type + + def to_dict(self): + json_dict = { + 'latitude': self.latitude, + 'longitude': self.longitude, + 'title': self.title, + 'address' : self.address + } + if self.foursquare_id: + json_dict['foursquare_id'] = self.foursquare_id + if self.foursquare_type: + json_dict['foursquare_type'] = self.foursquare_type + if self.google_place_id: + json_dict['google_place_id'] = self.google_place_id + if self.google_place_type: + json_dict['google_place_type'] = self.google_place_type + return json_dict + + +class InputContactMessageContent(Dictionaryable): + def __init__(self, phone_number, first_name, last_name=None, vcard=None): + self.phone_number: str = phone_number + self.first_name: str = first_name + self.last_name: str = last_name + self.vcard: str = vcard + + def to_dict(self): + json_dict = {'phone_number': self.phone_number, 'first_name': self.first_name} + if self.last_name: + json_dict['last_name'] = self.last_name + if self.vcard: + json_dict['vcard'] = self.vcard + return json_dict + + +class InputInvoiceMessageContent(Dictionaryable): + def __init__(self, title, description, payload, provider_token, currency, prices, + max_tip_amount=None, suggested_tip_amounts=None, provider_data=None, + photo_url=None, photo_size=None, photo_width=None, photo_height=None, + need_name=None, need_phone_number=None, need_email=None, need_shipping_address=None, + send_phone_number_to_provider=None, send_email_to_provider=None, + is_flexible=None): + self.title: str = title + self.description: str = description + self.payload: str = payload + self.provider_token: str = provider_token + self.currency: str = currency + self.prices: List[LabeledPrice] = prices + self.max_tip_amount: Optional[int] = max_tip_amount + self.suggested_tip_amounts: Optional[List[int]] = suggested_tip_amounts + self.provider_data: Optional[str] = provider_data + self.photo_url: Optional[str] = photo_url + self.photo_size: Optional[int] = photo_size + self.photo_width: Optional[int] = photo_width + self.photo_height: Optional[int] = photo_height + self.need_name: Optional[bool] = need_name + self.need_phone_number: Optional[bool] = need_phone_number + self.need_email: Optional[bool] = need_email + self.need_shipping_address: Optional[bool] = need_shipping_address + self.send_phone_number_to_provider: Optional[bool] = send_phone_number_to_provider + self.send_email_to_provider: Optional[bool] = send_email_to_provider + self.is_flexible: Optional[bool] = is_flexible + + def to_dict(self): + json_dict = { + 'title': self.title, + 'description': self.description, + 'payload': self.payload, + 'provider_token': self.provider_token, + 'currency': self.currency, + 'prices': [LabeledPrice.to_dict(lp) for lp in self.prices] + } + if self.max_tip_amount: + json_dict['max_tip_amount'] = self.max_tip_amount + if self.suggested_tip_amounts: + json_dict['suggested_tip_amounts'] = self.suggested_tip_amounts + if self.provider_data: + json_dict['provider_data'] = self.provider_data + if self.photo_url: + json_dict['photo_url'] = self.photo_url + if self.photo_size: + json_dict['photo_size'] = self.photo_size + if self.photo_width: + json_dict['photo_width'] = self.photo_width + if self.photo_height: + json_dict['photo_height'] = self.photo_height + if self.need_name is not None: + json_dict['need_name'] = self.need_name + if self.need_phone_number is not None: + json_dict['need_phone_number'] = self.need_phone_number + if self.need_email is not None: + json_dict['need_email'] = self.need_email + if self.need_shipping_address is not None: + json_dict['need_shipping_address'] = self.need_shipping_address + if self.send_phone_number_to_provider is not None: + json_dict['send_phone_number_to_provider'] = self.send_phone_number_to_provider + if self.send_email_to_provider is not None: + json_dict['send_email_to_provider'] = self.send_email_to_provider + if self.is_flexible is not None: + json_dict['is_flexible'] = self.is_flexible + return json_dict + + +class ChosenInlineResult(JsonDeserializable): + @classmethod + def de_json(cls, json_string): + if json_string is None: return None + obj = cls.check_json(json_string) + obj['from_user'] = User.de_json(obj.pop('from')) + if 'location' in obj: + obj['location'] = Location.de_json(obj['location']) + return cls(**obj) + + def __init__(self, result_id, from_user, query, location=None, inline_message_id=None, **kwargs): + """ + This object represents a result of an inline query + that was chosen by the user and sent to their chat partner. + :param result_id: string The unique identifier for the result that was chosen. + :param from_user: User The user that chose the result. + :param query: String The query that was used to obtain the result. + :return: ChosenInlineResult Object. + """ + self.result_id: str = result_id + self.from_user: User = from_user + self.location: Location = location + self.inline_message_id: str = inline_message_id + self.query: str = query + + +class InlineQueryResultBase(ABC, Dictionaryable, JsonSerializable): + # noinspection PyShadowingBuiltins + def __init__(self, type, id, title = None, caption = None, input_message_content = None, + reply_markup = None, caption_entities = None, parse_mode = None): + self.type = type + self.id = id + self.title = title + self.caption = caption + self.input_message_content = input_message_content + self.reply_markup = reply_markup + self.caption_entities = caption_entities + self.parse_mode = parse_mode + + def to_json(self): + return json.dumps(self.to_dict()) + + def to_dict(self): + json_dict = { + 'type': self.type, + 'id': self.id + } + if self.title: + json_dict['title'] = self.title + if self.caption: + json_dict['caption'] = self.caption + if self.input_message_content: + json_dict['input_message_content'] = self.input_message_content.to_dict() + if self.reply_markup: + json_dict['reply_markup'] = self.reply_markup.to_dict() + if self.caption_entities: + json_dict['caption_entities'] = MessageEntity.to_list_of_dicts(self.caption_entities) + if self.parse_mode: + json_dict['parse_mode'] = self.parse_mode + return json_dict + + +class InlineQueryResultArticle(InlineQueryResultBase): + def __init__(self, id, title, input_message_content, reply_markup=None, + url=None, hide_url=None, description=None, thumb_url=None, thumb_width=None, thumb_height=None): + """ + Represents a link to an article or web page. + :param id: Unique identifier for this result, 1-64 Bytes. + :param title: Title of the result. + :param input_message_content: InputMessageContent : Content of the message to be sent + :param reply_markup: InlineKeyboardMarkup : Inline keyboard attached to the message + :param url: URL of the result. + :param hide_url: Pass True, if you don't want the URL to be shown in the message. + :param description: Short description of the result. + :param thumb_url: Url of the thumbnail for the result. + :param thumb_width: Thumbnail width. + :param thumb_height: Thumbnail height + :return: + """ + super().__init__('article', id, title = title, input_message_content = input_message_content, reply_markup = reply_markup) + self.url = url + self.hide_url = hide_url + self.description = description + self.thumb_url = thumb_url + self.thumb_width = thumb_width + self.thumb_height = thumb_height + + def to_dict(self): + json_dict = super().to_dict() + if self.url: + json_dict['url'] = self.url + if self.hide_url: + json_dict['hide_url'] = self.hide_url + if self.description: + json_dict['description'] = self.description + if self.thumb_url: + json_dict['thumb_url'] = self.thumb_url + if self.thumb_width: + json_dict['thumb_width'] = self.thumb_width + if self.thumb_height: + json_dict['thumb_height'] = self.thumb_height + return json_dict + + +class InlineQueryResultPhoto(InlineQueryResultBase): + def __init__(self, id, photo_url, thumb_url, photo_width=None, photo_height=None, title=None, + description=None, caption=None, caption_entities=None, parse_mode=None, reply_markup=None, input_message_content=None): + """ + Represents a link to a photo. + :param id: Unique identifier for this result, 1-64 bytes + :param photo_url: A valid URL of the photo. Photo must be in jpeg format. Photo size must not exceed 5MB + :param thumb_url: URL of the thumbnail for the photo + :param photo_width: Width of the photo. + :param photo_height: Height of the photo. + :param title: Title for the result. + :param description: Short description of the result. + :param caption: Caption of the photo to be sent, 0-200 characters. + :param parse_mode: Send Markdown or HTML, if you want Telegram apps to show bold, italic, fixed-width text or + inline URLs in the media caption. + :param reply_markup: InlineKeyboardMarkup : Inline keyboard attached to the message + :param input_message_content: InputMessageContent : Content of the message to be sent instead of the photo + :return: + """ + super().__init__('photo', id, title = title, caption = caption, + input_message_content = input_message_content, reply_markup = reply_markup, + parse_mode = parse_mode, caption_entities = caption_entities) + self.photo_url = photo_url + self.thumb_url = thumb_url + self.photo_width = photo_width + self.photo_height = photo_height + self.description = description + + def to_dict(self): + json_dict = super().to_dict() + json_dict['photo_url'] = self.photo_url + json_dict['thumb_url'] = self.thumb_url + if self.photo_width: + json_dict['photo_width'] = self.photo_width + if self.photo_height: + json_dict['photo_height'] = self.photo_height + if self.description: + json_dict['description'] = self.description + return json_dict + + +class InlineQueryResultGif(InlineQueryResultBase): + def __init__(self, id, gif_url, thumb_url, gif_width=None, gif_height=None, + title=None, caption=None, caption_entities=None, + reply_markup=None, input_message_content=None, gif_duration=None, parse_mode=None, + thumb_mime_type=None): + """ + Represents a link to an animated GIF file. + :param id: Unique identifier for this result, 1-64 bytes. + :param gif_url: A valid URL for the GIF file. File size must not exceed 1MB + :param thumb_url: URL of the static thumbnail (jpeg or gif) for the result. + :param gif_width: Width of the GIF. + :param gif_height: Height of the GIF. + :param title: Title for the result. + :param caption: Caption of the GIF file to be sent, 0-200 characters + :param reply_markup: InlineKeyboardMarkup : Inline keyboard attached to the message + :param input_message_content: InputMessageContent : Content of the message to be sent instead of the photo + :return: + """ + super().__init__('gif', id, title = title, caption = caption, + input_message_content = input_message_content, reply_markup = reply_markup, + parse_mode = parse_mode, caption_entities = caption_entities) + self.gif_url = gif_url + self.gif_width = gif_width + self.gif_height = gif_height + self.thumb_url = thumb_url + self.gif_duration = gif_duration + self.thumb_mime_type = thumb_mime_type + + def to_dict(self): + json_dict = super().to_dict() + json_dict['gif_url'] = self.gif_url + if self.gif_width: + json_dict['gif_width'] = self.gif_width + if self.gif_height: + json_dict['gif_height'] = self.gif_height + json_dict['thumb_url'] = self.thumb_url + if self.gif_duration: + json_dict['gif_duration'] = self.gif_duration + if self.thumb_mime_type: + json_dict['thumb_mime_type'] = self.thumb_mime_type + return json_dict + + +class InlineQueryResultMpeg4Gif(InlineQueryResultBase): + def __init__(self, id, mpeg4_url, thumb_url, mpeg4_width=None, mpeg4_height=None, + title=None, caption=None, caption_entities=None, + parse_mode=None, reply_markup=None, input_message_content=None, mpeg4_duration=None, + thumb_mime_type=None): + """ + Represents a link to a video animation (H.264/MPEG-4 AVC video without sound). + :param id: Unique identifier for this result, 1-64 bytes + :param mpeg4_url: A valid URL for the MP4 file. File size must not exceed 1MB + :param thumb_url: URL of the static thumbnail (jpeg or gif) for the result + :param mpeg4_width: Video width + :param mpeg4_height: Video height + :param title: Title for the result + :param caption: Caption of the MPEG-4 file to be sent, 0-200 characters + :param parse_mode: Send Markdown or HTML, if you want Telegram apps to show bold, italic, fixed-width text + or inline URLs in the media caption. + :param reply_markup: InlineKeyboardMarkup : Inline keyboard attached to the message + :param input_message_content: InputMessageContent : Content of the message to be sent instead of the photo + :return: + """ + super().__init__('mpeg4_gif', id, title = title, caption = caption, + input_message_content = input_message_content, reply_markup = reply_markup, + parse_mode = parse_mode, caption_entities = caption_entities) + self.mpeg4_url = mpeg4_url + self.mpeg4_width = mpeg4_width + self.mpeg4_height = mpeg4_height + self.thumb_url = thumb_url + self.mpeg4_duration = mpeg4_duration + self.thumb_mime_type = thumb_mime_type + + def to_dict(self): + json_dict = super().to_dict() + json_dict['mpeg4_url'] = self.mpeg4_url + if self.mpeg4_width: + json_dict['mpeg4_width'] = self.mpeg4_width + if self.mpeg4_height: + json_dict['mpeg4_height'] = self.mpeg4_height + json_dict['thumb_url'] = self.thumb_url + if self.mpeg4_duration: + json_dict['mpeg4_duration '] = self.mpeg4_duration + if self.thumb_mime_type: + json_dict['thumb_mime_type'] = self.thumb_mime_type + return json_dict + + +class InlineQueryResultVideo(InlineQueryResultBase): + def __init__(self, id, video_url, mime_type, thumb_url, + title, caption=None, caption_entities=None, parse_mode=None, + video_width=None, video_height=None, video_duration=None, + description=None, reply_markup=None, input_message_content=None): + """ + Represents link to a page containing an embedded video player or a video file. + :param id: Unique identifier for this result, 1-64 bytes + :param video_url: A valid URL for the embedded video player or video file + :param mime_type: Mime type of the content of video url, “text/html” or “video/mp4” + :param thumb_url: URL of the thumbnail (jpeg only) for the video + :param title: Title for the result + :param parse_mode: Send Markdown or HTML, if you want Telegram apps to show bold, italic, fixed-width text or + inline URLs in the media caption. + :param video_width: Video width + :param video_height: Video height + :param video_duration: Video duration in seconds + :param description: Short description of the result + :return: + """ + super().__init__('video', id, title = title, caption = caption, + input_message_content = input_message_content, reply_markup = reply_markup, + parse_mode = parse_mode, caption_entities = caption_entities) + self.video_url = video_url + self.mime_type = mime_type + self.thumb_url = thumb_url + self.video_width = video_width + self.video_height = video_height + self.video_duration = video_duration + self.description = description + + def to_dict(self): + json_dict = super().to_dict() + json_dict['video_url'] = self.video_url + json_dict['mime_type'] = self.mime_type + json_dict['thumb_url'] = self.thumb_url + if self.video_height: + json_dict['video_height'] = self.video_height + if self.video_duration: + json_dict['video_duration'] = self.video_duration + if self.description: + json_dict['description'] = self.description + return json_dict + + +class InlineQueryResultAudio(InlineQueryResultBase): + def __init__(self, id, audio_url, title, + caption=None, caption_entities=None, parse_mode=None, performer=None, + audio_duration=None, reply_markup=None, input_message_content=None): + super().__init__('audio', id, title = title, caption = caption, + input_message_content = input_message_content, reply_markup = reply_markup, + parse_mode = parse_mode, caption_entities = caption_entities) + self.audio_url = audio_url + self.performer = performer + self.audio_duration = audio_duration + + def to_dict(self): + json_dict = super().to_dict() + json_dict['audio_url'] = self.audio_url + if self.performer: + json_dict['performer'] = self.performer + if self.audio_duration: + json_dict['audio_duration'] = self.audio_duration + return json_dict + + +class InlineQueryResultVoice(InlineQueryResultBase): + def __init__(self, id, voice_url, title, caption=None, caption_entities=None, + parse_mode=None, voice_duration=None, reply_markup=None, input_message_content=None): + super().__init__('voice', id, title = title, caption = caption, + input_message_content = input_message_content, reply_markup = reply_markup, + parse_mode = parse_mode, caption_entities = caption_entities) + self.voice_url = voice_url + self.voice_duration = voice_duration + + def to_dict(self): + json_dict = super().to_dict() + json_dict['voice_url'] = self.voice_url + if self.voice_duration: + json_dict['voice_duration'] = self.voice_duration + return json_dict + + +class InlineQueryResultDocument(InlineQueryResultBase): + def __init__(self, id, title, document_url, mime_type, caption=None, caption_entities=None, + parse_mode=None, description=None, reply_markup=None, input_message_content=None, + thumb_url=None, thumb_width=None, thumb_height=None): + super().__init__('document', id, title = title, caption = caption, + input_message_content = input_message_content, reply_markup = reply_markup, + parse_mode = parse_mode, caption_entities = caption_entities) + self.document_url = document_url + self.mime_type = mime_type + self.description = description + self.thumb_url = thumb_url + self.thumb_width = thumb_width + self.thumb_height = thumb_height + + def to_dict(self): + json_dict = super().to_dict() + json_dict['document_url'] = self.document_url + json_dict['mime_type'] = self.mime_type + if self.description: + json_dict['description'] = self.description + if self.thumb_url: + json_dict['thumb_url'] = self.thumb_url + if self.thumb_width: + json_dict['thumb_width'] = self.thumb_width + if self.thumb_height: + json_dict['thumb_height'] = self.thumb_height + return json_dict + + +class InlineQueryResultLocation(InlineQueryResultBase): + def __init__(self, id, title, latitude, longitude, horizontal_accuracy, live_period=None, reply_markup=None, + input_message_content=None, thumb_url=None, thumb_width=None, thumb_height=None, heading=None, proximity_alert_radius = None): + super().__init__('location', id, title = title, + input_message_content = input_message_content, reply_markup = reply_markup) + self.latitude = latitude + self.longitude = longitude + self.horizontal_accuracy = horizontal_accuracy + self.live_period = live_period + self.heading: int = heading + self.proximity_alert_radius: int = proximity_alert_radius + self.thumb_url = thumb_url + self.thumb_width = thumb_width + self.thumb_height = thumb_height + + def to_dict(self): + json_dict = super().to_dict() + json_dict['latitude'] = self.latitude + json_dict['longitude'] = self.longitude + if self.horizontal_accuracy: + json_dict['horizontal_accuracy'] = self.horizontal_accuracy + if self.live_period: + json_dict['live_period'] = self.live_period + if self.heading: + json_dict['heading'] = self.heading + if self.proximity_alert_radius: + json_dict['proximity_alert_radius'] = self.proximity_alert_radius + if self.thumb_url: + json_dict['thumb_url'] = self.thumb_url + if self.thumb_width: + json_dict['thumb_width'] = self.thumb_width + if self.thumb_height: + json_dict['thumb_height'] = self.thumb_height + return json_dict + + +class InlineQueryResultVenue(InlineQueryResultBase): + def __init__(self, id, title, latitude, longitude, address, foursquare_id=None, foursquare_type=None, + reply_markup=None, input_message_content=None, thumb_url=None, + thumb_width=None, thumb_height=None, google_place_id=None, google_place_type=None): + super().__init__('venue', id, title = title, + input_message_content = input_message_content, reply_markup = reply_markup) + self.latitude = latitude + self.longitude = longitude + self.address = address + self.foursquare_id = foursquare_id + self.foursquare_type = foursquare_type + self.google_place_id = google_place_id + self.google_place_type = google_place_type + self.thumb_url = thumb_url + self.thumb_width = thumb_width + self.thumb_height = thumb_height + + def to_dict(self): + json_dict = super().to_dict() + json_dict['latitude'] = self.latitude + json_dict['longitude'] = self.longitude + json_dict['address'] = self.address + if self.foursquare_id: + json_dict['foursquare_id'] = self.foursquare_id + if self.foursquare_type: + json_dict['foursquare_type'] = self.foursquare_type + if self.google_place_id: + json_dict['google_place_id'] = self.google_place_id + if self.google_place_type: + json_dict['google_place_type'] = self.google_place_type + if self.thumb_url: + json_dict['thumb_url'] = self.thumb_url + if self.thumb_width: + json_dict['thumb_width'] = self.thumb_width + if self.thumb_height: + json_dict['thumb_height'] = self.thumb_height + return json_dict + + +class InlineQueryResultContact(InlineQueryResultBase): + def __init__(self, id, phone_number, first_name, last_name=None, vcard=None, + reply_markup=None, input_message_content=None, + thumb_url=None, thumb_width=None, thumb_height=None): + super().__init__('contact', id, + input_message_content = input_message_content, reply_markup = reply_markup) + self.phone_number = phone_number + self.first_name = first_name + self.last_name = last_name + self.vcard = vcard + self.thumb_url = thumb_url + self.thumb_width = thumb_width + self.thumb_height = thumb_height + + def to_dict(self): + json_dict = super().to_dict() + json_dict['phone_number'] = self.phone_number + json_dict['first_name'] = self.first_name + if self.last_name: + json_dict['last_name'] = self.last_name + if self.vcard: + json_dict['vcard'] = self.vcard + if self.thumb_url: + json_dict['thumb_url'] = self.thumb_url + if self.thumb_width: + json_dict['thumb_width'] = self.thumb_width + if self.thumb_height: + json_dict['thumb_height'] = self.thumb_height + return json_dict + + +class InlineQueryResultGame(InlineQueryResultBase): + def __init__(self, id, game_short_name, reply_markup=None): + super().__init__('game', id, reply_markup = reply_markup) + self.game_short_name = game_short_name + + def to_dict(self): + json_dict = super().to_dict() + json_dict['game_short_name'] = self.game_short_name + return json_dict + + +class InlineQueryResultCachedBase(ABC, JsonSerializable): + def __init__(self): + self.type = None + self.id = None + self.title = None + self.description = None + self.caption = None + self.reply_markup = None + self.input_message_content = None + self.parse_mode = None + self.caption_entities = None + self.payload_dic = {} + + def to_json(self): + json_dict = self.payload_dic + json_dict['type'] = self.type + json_dict['id'] = self.id + if self.title: + json_dict['title'] = self.title + if self.description: + json_dict['description'] = self.description + if self.caption: + json_dict['caption'] = self.caption + if self.reply_markup: + json_dict['reply_markup'] = self.reply_markup.to_dict() + if self.input_message_content: + json_dict['input_message_content'] = self.input_message_content.to_dict() + if self.parse_mode: + json_dict['parse_mode'] = self.parse_mode + if self.caption_entities: + json_dict['caption_entities'] = MessageEntity.to_list_of_dicts(self.caption_entities) + return json.dumps(json_dict) + + +class InlineQueryResultCachedPhoto(InlineQueryResultCachedBase): + def __init__(self, id, photo_file_id, title=None, description=None, + caption=None, caption_entities = None, parse_mode=None, + reply_markup=None, input_message_content=None): + InlineQueryResultCachedBase.__init__(self) + self.type = 'photo' + self.id = id + self.photo_file_id = photo_file_id + self.title = title + self.description = description + self.caption = caption + self.caption_entities = caption_entities + self.reply_markup = reply_markup + self.input_message_content = input_message_content + self.parse_mode = parse_mode + self.payload_dic['photo_file_id'] = photo_file_id + + +class InlineQueryResultCachedGif(InlineQueryResultCachedBase): + def __init__(self, id, gif_file_id, title=None, description=None, + caption=None, caption_entities = None, parse_mode=None, + reply_markup=None, input_message_content=None): + InlineQueryResultCachedBase.__init__(self) + self.type = 'gif' + self.id = id + self.gif_file_id = gif_file_id + self.title = title + self.description = description + self.caption = caption + self.caption_entities = caption_entities + self.reply_markup = reply_markup + self.input_message_content = input_message_content + self.parse_mode = parse_mode + self.payload_dic['gif_file_id'] = gif_file_id + + +class InlineQueryResultCachedMpeg4Gif(InlineQueryResultCachedBase): + def __init__(self, id, mpeg4_file_id, title=None, description=None, + caption=None, caption_entities = None, parse_mode=None, + reply_markup=None, input_message_content=None): + InlineQueryResultCachedBase.__init__(self) + self.type = 'mpeg4_gif' + self.id = id + self.mpeg4_file_id = mpeg4_file_id + self.title = title + self.description = description + self.caption = caption + self.caption_entities = caption_entities + self.reply_markup = reply_markup + self.input_message_content = input_message_content + self.parse_mode = parse_mode + self.payload_dic['mpeg4_file_id'] = mpeg4_file_id + + +class InlineQueryResultCachedSticker(InlineQueryResultCachedBase): + def __init__(self, id, sticker_file_id, reply_markup=None, input_message_content=None): + InlineQueryResultCachedBase.__init__(self) + self.type = 'sticker' + self.id = id + self.sticker_file_id = sticker_file_id + self.reply_markup = reply_markup + self.input_message_content = input_message_content + self.payload_dic['sticker_file_id'] = sticker_file_id + + +class InlineQueryResultCachedDocument(InlineQueryResultCachedBase): + def __init__(self, id, document_file_id, title, description=None, + caption=None, caption_entities = None, parse_mode=None, + reply_markup=None, input_message_content=None): + InlineQueryResultCachedBase.__init__(self) + self.type = 'document' + self.id = id + self.document_file_id = document_file_id + self.title = title + self.description = description + self.caption = caption + self.caption_entities = caption_entities + self.reply_markup = reply_markup + self.input_message_content = input_message_content + self.parse_mode = parse_mode + self.payload_dic['document_file_id'] = document_file_id + + +class InlineQueryResultCachedVideo(InlineQueryResultCachedBase): + def __init__(self, id, video_file_id, title, description=None, + caption=None, caption_entities = None, parse_mode=None, + reply_markup=None, + input_message_content=None): + InlineQueryResultCachedBase.__init__(self) + self.type = 'video' + self.id = id + self.video_file_id = video_file_id + self.title = title + self.description = description + self.caption = caption + self.caption_entities = caption_entities + self.reply_markup = reply_markup + self.input_message_content = input_message_content + self.parse_mode = parse_mode + self.payload_dic['video_file_id'] = video_file_id + + +class InlineQueryResultCachedVoice(InlineQueryResultCachedBase): + def __init__(self, id, voice_file_id, title, caption=None, caption_entities = None, + parse_mode=None, reply_markup=None, input_message_content=None): + InlineQueryResultCachedBase.__init__(self) + self.type = 'voice' + self.id = id + self.voice_file_id = voice_file_id + self.title = title + self.caption = caption + self.caption_entities = caption_entities + self.reply_markup = reply_markup + self.input_message_content = input_message_content + self.parse_mode = parse_mode + self.payload_dic['voice_file_id'] = voice_file_id + + +class InlineQueryResultCachedAudio(InlineQueryResultCachedBase): + def __init__(self, id, audio_file_id, caption=None, caption_entities = None, + parse_mode=None, reply_markup=None, input_message_content=None): + InlineQueryResultCachedBase.__init__(self) + self.type = 'audio' + self.id = id + self.audio_file_id = audio_file_id + self.caption = caption + self.caption_entities = caption_entities + self.reply_markup = reply_markup + self.input_message_content = input_message_content + self.parse_mode = parse_mode + self.payload_dic['audio_file_id'] = audio_file_id + + +# Games + +class Game(JsonDeserializable): + @classmethod + def de_json(cls, json_string): + if (json_string is None): return None + obj = cls.check_json(json_string) + obj['photo'] = Game.parse_photo(obj['photo']) + if 'text_entities' in obj: + obj['text_entities'] = Game.parse_entities(obj['text_entities']) + if 'animation' in obj: + obj['animation'] = Animation.de_json(obj['animation']) + return cls(**obj) + + @classmethod + def parse_photo(cls, photo_size_array): + ret = [] + for ps in photo_size_array: + ret.append(PhotoSize.de_json(ps)) + return ret + + @classmethod + def parse_entities(cls, message_entity_array): + ret = [] + for me in message_entity_array: + ret.append(MessageEntity.de_json(me)) + return ret + + def __init__(self, title, description, photo, text=None, text_entities=None, animation=None, **kwargs): + self.title: str = title + self.description: str = description + self.photo: List[PhotoSize] = photo + self.text: str = text + self.text_entities: List[MessageEntity] = text_entities + self.animation: Animation = animation + + +class Animation(JsonDeserializable): + @classmethod + def de_json(cls, json_string): + if (json_string is None): return None + obj = cls.check_json(json_string) + if 'thumb' in obj and 'file_id' in obj['thumb']: + obj["thumb"] = PhotoSize.de_json(obj['thumb']) + else: + obj['thumb'] = None + return cls(**obj) + + def __init__(self, file_id, file_unique_id, width=None, height=None, duration=None, + thumb=None, file_name=None, mime_type=None, file_size=None, **kwargs): + self.file_id: str = file_id + self.file_unique_id: str = file_unique_id + self.width: int = width + self.height: int = height + self.duration: int = duration + self.thumb: PhotoSize = thumb + self.file_name: str = file_name + self.mime_type: str = mime_type + self.file_size: int = file_size + + +class GameHighScore(JsonDeserializable): + @classmethod + def de_json(cls, json_string): + if (json_string is None): return None + obj = cls.check_json(json_string) + obj['user'] = User.de_json(obj['user']) + return cls(**obj) + + def __init__(self, position, user, score, **kwargs): + self.position: int = position + self.user: User = user + self.score: int = score + + +# Payments + +class LabeledPrice(JsonSerializable): + def __init__(self, label, amount): + self.label: str = label + self.amount: int = amount + + def to_dict(self): + return { + 'label': self.label, 'amount': self.amount + } + + def to_json(self): + return json.dumps(self.to_dict()) + + +class Invoice(JsonDeserializable): + @classmethod + def de_json(cls, json_string): + if (json_string is None): return None + obj = cls.check_json(json_string, dict_copy=False) + return cls(**obj) + + def __init__(self, title, description, start_parameter, currency, total_amount, **kwargs): + self.title: str = title + self.description: str = description + self.start_parameter: str = start_parameter + self.currency: str = currency + self.total_amount: int = total_amount + + +class ShippingAddress(JsonDeserializable): + @classmethod + def de_json(cls, json_string): + if (json_string is None): return None + obj = cls.check_json(json_string, dict_copy=False) + return cls(**obj) + + def __init__(self, country_code, state, city, street_line1, street_line2, post_code, **kwargs): + self.country_code: str = country_code + self.state: str = state + self.city: str = city + self.street_line1: str = street_line1 + self.street_line2: str = street_line2 + self.post_code: str = post_code + + +class OrderInfo(JsonDeserializable): + @classmethod + def de_json(cls, json_string): + if (json_string is None): return None + obj = cls.check_json(json_string) + obj['shipping_address'] = ShippingAddress.de_json(obj.get('shipping_address')) + return cls(**obj) + + def __init__(self, name=None, phone_number=None, email=None, shipping_address=None, **kwargs): + self.name: str = name + self.phone_number: str = phone_number + self.email: str = email + self.shipping_address: ShippingAddress = shipping_address + + +class ShippingOption(JsonSerializable): + def __init__(self, id, title): + self.id: str = id + self.title: str = title + self.prices: List[LabeledPrice] = [] + + def add_price(self, *args): + """ + Add LabeledPrice to ShippingOption + :param args: LabeledPrices + """ + for price in args: + self.prices.append(price) + return self + + def to_json(self): + price_list = [] + for p in self.prices: + price_list.append(p.to_dict()) + json_dict = json.dumps({'id': self.id, 'title': self.title, 'prices': price_list}) + return json_dict + + +class SuccessfulPayment(JsonDeserializable): + @classmethod + def de_json(cls, json_string): + if (json_string is None): return None + obj = cls.check_json(json_string) + obj['order_info'] = OrderInfo.de_json(obj.get('order_info')) + return cls(**obj) + + def __init__(self, currency, total_amount, invoice_payload, shipping_option_id=None, order_info=None, + telegram_payment_charge_id=None, provider_payment_charge_id=None, **kwargs): + self.currency: str = currency + self.total_amount: int = total_amount + self.invoice_payload: str = invoice_payload + self.shipping_option_id: str = shipping_option_id + self.order_info: OrderInfo = order_info + self.telegram_payment_charge_id: str = telegram_payment_charge_id + self.provider_payment_charge_id: str = provider_payment_charge_id + + +class ShippingQuery(JsonDeserializable): + @classmethod + def de_json(cls, json_string): + if (json_string is None): return None + obj = cls.check_json(json_string) + obj['from_user'] = User.de_json(obj.pop('from')) + obj['shipping_address'] = ShippingAddress.de_json(obj['shipping_address']) + return cls(**obj) + + def __init__(self, id, from_user, invoice_payload, shipping_address, **kwargs): + self.id: str = id + self.from_user: User = from_user + self.invoice_payload: str = invoice_payload + self.shipping_address: ShippingAddress = shipping_address + + +class PreCheckoutQuery(JsonDeserializable): + @classmethod + def de_json(cls, json_string): + if (json_string is None): return None + obj = cls.check_json(json_string) + obj['from_user'] = User.de_json(obj.pop('from')) + obj['order_info'] = OrderInfo.de_json(obj.get('order_info')) + return cls(**obj) + + def __init__(self, id, from_user, currency, total_amount, invoice_payload, shipping_option_id=None, order_info=None, **kwargs): + self.id: str = id + self.from_user: User = from_user + self.currency: str = currency + self.total_amount: int = total_amount + self.invoice_payload: str = invoice_payload + self.shipping_option_id: str = shipping_option_id + self.order_info: OrderInfo = order_info + + +# Stickers + +class StickerSet(JsonDeserializable): + @classmethod + def de_json(cls, json_string): + if (json_string is None): return None + obj = cls.check_json(json_string) + stickers = [] + for s in obj['stickers']: + stickers.append(Sticker.de_json(s)) + obj['stickers'] = stickers + if 'thumb' in obj and 'file_id' in obj['thumb']: + obj['thumb'] = PhotoSize.de_json(obj['thumb']) + else: + obj['thumb'] = None + return cls(**obj) + + def __init__(self, name, title, is_animated, is_video, contains_masks, stickers, thumb=None, **kwargs): + self.name: str = name + self.title: str = title + self.is_animated: bool = is_animated + self.is_video: bool = is_video + self.contains_masks: bool = contains_masks + self.stickers: List[Sticker] = stickers + self.thumb: PhotoSize = thumb + + +class Sticker(JsonDeserializable): + @classmethod + def de_json(cls, json_string): + if (json_string is None): return None + obj = cls.check_json(json_string) + if 'thumb' in obj and 'file_id' in obj['thumb']: + obj['thumb'] = PhotoSize.de_json(obj['thumb']) + else: + obj['thumb'] = None + if 'mask_position' in obj: + obj['mask_position'] = MaskPosition.de_json(obj['mask_position']) + return cls(**obj) + + def __init__(self, file_id, file_unique_id, width, height, is_animated, + is_video, thumb=None, emoji=None, set_name=None, mask_position=None, file_size=None, **kwargs): + self.file_id: str = file_id + self.file_unique_id: str = file_unique_id + self.width: int = width + self.height: int = height + self.is_animated: bool = is_animated + self.is_video: bool = is_video + self.thumb: PhotoSize = thumb + self.emoji: str = emoji + self.set_name: str = set_name + self.mask_position: MaskPosition = mask_position + self.file_size: int = file_size + + + +class MaskPosition(Dictionaryable, JsonDeserializable, JsonSerializable): + @classmethod + def de_json(cls, json_string): + if (json_string is None): return None + obj = cls.check_json(json_string, dict_copy=False) + return cls(**obj) + + def __init__(self, point, x_shift, y_shift, scale, **kwargs): + self.point: str = point + self.x_shift: float = x_shift + self.y_shift: float = y_shift + self.scale: float = scale + + def to_json(self): + return json.dumps(self.to_dict()) + + def to_dict(self): + return {'point': self.point, 'x_shift': self.x_shift, 'y_shift': self.y_shift, 'scale': self.scale} + + +# InputMedia + +class InputMedia(Dictionaryable, JsonSerializable): + def __init__(self, type, media, caption=None, parse_mode=None, caption_entities=None): + self.type: str = type + self.media: str = media + self.caption: Optional[str] = caption + self.parse_mode: Optional[str] = parse_mode + self.caption_entities: Optional[List[MessageEntity]] = caption_entities + + if util.is_string(self.media): + self._media_name = '' + self._media_dic = self.media + else: + self._media_name = util.generate_random_token() + self._media_dic = 'attach://{0}'.format(self._media_name) + + def to_json(self): + return json.dumps(self.to_dict()) + + def to_dict(self): + json_dict = {'type': self.type, 'media': self._media_dic} + if self.caption: + json_dict['caption'] = self.caption + if self.parse_mode: + json_dict['parse_mode'] = self.parse_mode + if self.caption_entities: + json_dict['caption_entities'] = MessageEntity.to_list_of_dicts(self.caption_entities) + return json_dict + + def convert_input_media(self): + if util.is_string(self.media): + return self.to_json(), None + + return self.to_json(), {self._media_name: self.media} + + +class InputMediaPhoto(InputMedia): + def __init__(self, media, caption=None, parse_mode=None): + if util.is_pil_image(media): + media = util.pil_image_to_file(media) + + super(InputMediaPhoto, self).__init__(type="photo", media=media, caption=caption, parse_mode=parse_mode) + + def to_dict(self): + return super(InputMediaPhoto, self).to_dict() + + +class InputMediaVideo(InputMedia): + def __init__(self, media, thumb=None, caption=None, parse_mode=None, width=None, height=None, duration=None, + supports_streaming=None): + super(InputMediaVideo, self).__init__(type="video", media=media, caption=caption, parse_mode=parse_mode) + self.thumb = thumb + self.width = width + self.height = height + self.duration = duration + self.supports_streaming = supports_streaming + + def to_dict(self): + ret = super(InputMediaVideo, self).to_dict() + if self.thumb: + ret['thumb'] = self.thumb + if self.width: + ret['width'] = self.width + if self.height: + ret['height'] = self.height + if self.duration: + ret['duration'] = self.duration + if self.supports_streaming: + ret['supports_streaming'] = self.supports_streaming + return ret + + +class InputMediaAnimation(InputMedia): + def __init__(self, media, thumb=None, caption=None, parse_mode=None, width=None, height=None, duration=None): + super(InputMediaAnimation, self).__init__(type="animation", media=media, caption=caption, parse_mode=parse_mode) + self.thumb = thumb + self.width = width + self.height = height + self.duration = duration + + def to_dict(self): + ret = super(InputMediaAnimation, self).to_dict() + if self.thumb: + ret['thumb'] = self.thumb + if self.width: + ret['width'] = self.width + if self.height: + ret['height'] = self.height + if self.duration: + ret['duration'] = self.duration + return ret + + +class InputMediaAudio(InputMedia): + def __init__(self, media, thumb=None, caption=None, parse_mode=None, duration=None, performer=None, title=None): + super(InputMediaAudio, self).__init__(type="audio", media=media, caption=caption, parse_mode=parse_mode) + self.thumb = thumb + self.duration = duration + self.performer = performer + self.title = title + + def to_dict(self): + ret = super(InputMediaAudio, self).to_dict() + if self.thumb: + ret['thumb'] = self.thumb + if self.duration: + ret['duration'] = self.duration + if self.performer: + ret['performer'] = self.performer + if self.title: + ret['title'] = self.title + return ret + + +class InputMediaDocument(InputMedia): + def __init__(self, media, thumb=None, caption=None, parse_mode=None, disable_content_type_detection=None): + super(InputMediaDocument, self).__init__(type="document", media=media, caption=caption, parse_mode=parse_mode) + self.thumb = thumb + self.disable_content_type_detection = disable_content_type_detection + + def to_dict(self): + ret = super(InputMediaDocument, self).to_dict() + if self.thumb: + ret['thumb'] = self.thumb + if self.disable_content_type_detection is not None: + ret['disable_content_type_detection'] = self.disable_content_type_detection + return ret + + +class PollOption(JsonDeserializable): + @classmethod + def de_json(cls, json_string): + if (json_string is None): return None + obj = cls.check_json(json_string, dict_copy=False) + return cls(**obj) + + def __init__(self, text, voter_count = 0, **kwargs): + self.text: str = text + self.voter_count: int = voter_count + # Converted in _convert_poll_options + # def to_json(self): + # # send_poll Option is a simple string: https://core.telegram.org/bots/api#sendpoll + # return json.dumps(self.text) + + +class Poll(JsonDeserializable): + @classmethod + def de_json(cls, json_string): + if (json_string is None): return None + obj = cls.check_json(json_string) + obj['poll_id'] = obj.pop('id') + options = [] + for opt in obj['options']: + options.append(PollOption.de_json(opt)) + obj['options'] = options or None + if 'explanation_entities' in obj: + obj['explanation_entities'] = Message.parse_entities(obj['explanation_entities']) + return cls(**obj) + + def __init__( + self, + question, options, + poll_id=None, total_voter_count=None, is_closed=None, is_anonymous=None, poll_type=None, + allows_multiple_answers=None, correct_option_id=None, explanation=None, explanation_entities=None, + open_period=None, close_date=None, **kwargs): + self.id: str = poll_id + self.question: str = question + self.options: List[PollOption] = options + self.total_voter_count: int = total_voter_count + self.is_closed: bool = is_closed + self.is_anonymous: bool = is_anonymous + self.type: str = poll_type + self.allows_multiple_answers: bool = allows_multiple_answers + self.correct_option_id: int = correct_option_id + self.explanation: str = explanation + self.explanation_entities: List[MessageEntity] = explanation_entities # Default state of entities is None. if (explanation_entities is not None) else [] + self.open_period: int = open_period + self.close_date: int = close_date + + def add(self, option): + if type(option) is PollOption: + self.options.append(option) + else: + self.options.append(PollOption(option)) + + +class PollAnswer(JsonSerializable, JsonDeserializable, Dictionaryable): + @classmethod + def de_json(cls, json_string): + if (json_string is None): return None + obj = cls.check_json(json_string) + obj['user'] = User.de_json(obj['user']) + return cls(**obj) + + def __init__(self, poll_id, user, option_ids, **kwargs): + self.poll_id: str = poll_id + self.user: User = user + self.option_ids: List[int] = option_ids + + def to_json(self): + return json.dumps(self.to_dict()) + + def to_dict(self): + return {'poll_id': self.poll_id, + 'user': self.user.to_dict(), + 'option_ids': self.option_ids} + + +class ChatLocation(JsonSerializable, JsonDeserializable, Dictionaryable): + @classmethod + def de_json(cls, json_string): + if json_string is None: return json_string + obj = cls.check_json(json_string) + obj['location'] = Location.de_json(obj['location']) + return cls(**obj) + + def __init__(self, location, address, **kwargs): + self.location: Location = location + self.address: str = address + + def to_json(self): + return json.dumps(self.to_dict()) + + def to_dict(self): + return { + "location": self.location.to_dict(), + "address": self.address + } + + +class ChatInviteLink(JsonSerializable, JsonDeserializable, Dictionaryable): + @classmethod + def de_json(cls, json_string): + if json_string is None: return None + obj = cls.check_json(json_string) + obj['creator'] = User.de_json(obj['creator']) + return cls(**obj) + + def __init__(self, invite_link, creator, creates_join_request , is_primary, is_revoked, + name=None, expire_date=None, member_limit=None, pending_join_request_count=None, **kwargs): + self.invite_link: str = invite_link + self.creator: User = creator + self.creates_join_request: bool = creates_join_request + self.is_primary: bool = is_primary + self.is_revoked: bool = is_revoked + self.name: str = name + self.expire_date: int = expire_date + self.member_limit: int = member_limit + self.pending_join_request_count: int = pending_join_request_count + + def to_json(self): + return json.dumps(self.to_dict()) + + def to_dict(self): + json_dict = { + "invite_link": self.invite_link, + "creator": self.creator.to_dict(), + "is_primary": self.is_primary, + "is_revoked": self.is_revoked, + "creates_join_request": self.creates_join_request + } + if self.expire_date: + json_dict["expire_date"] = self.expire_date + if self.member_limit: + json_dict["member_limit"] = self.member_limit + if self.pending_join_request_count: + json_dict["pending_join_request_count"] = self.pending_join_request_count + if self.name: + json_dict["name"] = self.name + return json_dict + + +class ProximityAlertTriggered(JsonDeserializable): + @classmethod + def de_json(cls, json_string): + if json_string is None: return None + obj = cls.check_json(json_string, dict_copy=False) + return cls(**obj) + + def __init__(self, traveler, watcher, distance, **kwargs): + self.traveler: User = traveler + self.watcher: User = watcher + self.distance: int = distance + + +class VoiceChatStarted(JsonDeserializable): + @classmethod + def de_json(cls, json_string): + return cls() + + def __init__(self): + """ + This object represents a service message about a voice chat started in the chat. + Currently holds no information. + """ + pass + + +class VoiceChatScheduled(JsonDeserializable): + @classmethod + def de_json(cls, json_string): + if json_string is None: return None + obj = cls.check_json(json_string, dict_copy=False) + return cls(**obj) + + def __init__(self, start_date, **kwargs): + self.start_date: int = start_date + + +class VoiceChatEnded(JsonDeserializable): + @classmethod + def de_json(cls, json_string): + if json_string is None: return None + obj = cls.check_json(json_string, dict_copy=False) + return cls(**obj) + + def __init__(self, duration, **kwargs): + self.duration: int = duration + + +class VoiceChatParticipantsInvited(JsonDeserializable): + @classmethod + def de_json(cls, json_string): + if json_string is None: return None + obj = cls.check_json(json_string) + if 'users' in obj: + obj['users'] = [User.de_json(u) for u in obj['users']] + return cls(**obj) + + def __init__(self, users=None, **kwargs): + self.users: List[User] = users + + +class MessageAutoDeleteTimerChanged(JsonDeserializable): + @classmethod + def de_json(cls, json_string): + if json_string is None: return None + obj = cls.check_json(json_string, dict_copy=False) + return cls(**obj) + + def __init__(self, message_auto_delete_time, **kwargs): + self.message_auto_delete_time = message_auto_delete_time diff --git a/telebot/util.py b/telebot/util.py new file mode 100644 index 0000000..14ed360 --- /dev/null +++ b/telebot/util.py @@ -0,0 +1,512 @@ +# -*- coding: utf-8 -*- +import random +import re +import string +import threading +import traceback +from typing import Any, Callable, List, Dict, Optional, Union + +# noinspection PyPep8Naming +import queue as Queue +import logging + +from telebot import types + +try: + import ujson as json +except ImportError: + import json + +try: + # noinspection PyPackageRequirements + from PIL import Image + from io import BytesIO + + pil_imported = True +except: + pil_imported = False + +MAX_MESSAGE_LENGTH = 4096 + +logger = logging.getLogger('TeleBot') + +thread_local = threading.local() + +content_type_media = [ + 'text', 'audio', 'animation', 'document', 'photo', 'sticker', 'video', 'video_note', 'voice', 'contact', 'dice', 'poll', + 'venue', 'location' +] + +content_type_service = [ + 'new_chat_members', 'left_chat_member', 'new_chat_title', 'new_chat_photo', 'delete_chat_photo', 'group_chat_created', + 'supergroup_chat_created', 'channel_chat_created', 'migrate_to_chat_id', 'migrate_from_chat_id', 'pinned_message', + 'proximity_alert_triggered', 'voice_chat_scheduled', 'voice_chat_started', 'voice_chat_ended', + 'voice_chat_participants_invited', 'message_auto_delete_timer_changed' +] + +update_types = [ + "update_id", "message", "edited_message", "channel_post", "edited_channel_post", "inline_query", + "chosen_inline_result", "callback_query", "shipping_query", "pre_checkout_query", "poll", "poll_answer", + "my_chat_member", "chat_member", "chat_join_request" +] + + +class WorkerThread(threading.Thread): + count = 0 + + def __init__(self, exception_callback=None, queue=None, name=None): + if not name: + name = "WorkerThread{0}".format(self.__class__.count + 1) + self.__class__.count += 1 + if not queue: + queue = Queue.Queue() + + threading.Thread.__init__(self, name=name) + self.queue = queue + self.daemon = True + + self.received_task_event = threading.Event() + self.done_event = threading.Event() + self.exception_event = threading.Event() + self.continue_event = threading.Event() + + self.exception_callback = exception_callback + self.exception_info = None + self._running = True + self.start() + + def run(self): + while self._running: + try: + task, args, kwargs = self.queue.get(block=True, timeout=.5) + self.continue_event.clear() + self.received_task_event.clear() + self.done_event.clear() + self.exception_event.clear() + logger.debug("Received task") + self.received_task_event.set() + + task(*args, **kwargs) + logger.debug("Task complete") + self.done_event.set() + except Queue.Empty: + pass + except Exception as e: + logger.debug(type(e).__name__ + " occurred, args=" + str(e.args) + "\n" + traceback.format_exc()) + self.exception_info = e + self.exception_event.set() + if self.exception_callback: + self.exception_callback(self, self.exception_info) + self.continue_event.wait() + + def put(self, task, *args, **kwargs): + self.queue.put((task, args, kwargs)) + + def raise_exceptions(self): + if self.exception_event.is_set(): + raise self.exception_info + + def clear_exceptions(self): + self.exception_event.clear() + self.continue_event.set() + + def stop(self): + self._running = False + + +class ThreadPool: + + def __init__(self, telebot, num_threads=2): + self.telebot = telebot + self.tasks = Queue.Queue() + self.workers = [WorkerThread(self.on_exception, self.tasks) for _ in range(num_threads)] + self.num_threads = num_threads + + self.exception_event = threading.Event() + self.exception_info = None + + def put(self, func, *args, **kwargs): + self.tasks.put((func, args, kwargs)) + + def on_exception(self, worker_thread, exc_info): + if self.telebot.exception_handler is not None: + handled = self.telebot.exception_handler.handle(exc_info) + else: + handled = False + if not handled: + self.exception_info = exc_info + self.exception_event.set() + worker_thread.continue_event.set() + + def raise_exceptions(self): + if self.exception_event.is_set(): + raise self.exception_info + + def clear_exceptions(self): + self.exception_event.clear() + + def close(self): + for worker in self.workers: + worker.stop() + for worker in self.workers: + worker.join() + + +class AsyncTask: + def __init__(self, target, *args, **kwargs): + self.target = target + self.args = args + self.kwargs = kwargs + + self.done = False + self.thread = threading.Thread(target=self._run) + self.thread.start() + + def _run(self): + try: + self.result = self.target(*self.args, **self.kwargs) + except Exception as e: + self.result = e + self.done = True + + def wait(self): + if not self.done: + self.thread.join() + if isinstance(self.result, BaseException): + raise self.result + else: + return self.result + + +class CustomRequestResponse(): + def __init__(self, json_text, status_code = 200, reason = ""): + self.status_code = status_code + self.text = json_text + self.reason = reason + + def json(self): + return json.loads(self.text) + + +def async_dec(): + def decorator(fn): + def wrapper(*args, **kwargs): + return AsyncTask(fn, *args, **kwargs) + + return wrapper + + return decorator + + +def is_string(var): + return isinstance(var, str) + + +def is_dict(var): + return isinstance(var, dict) + + +def is_bytes(var): + return isinstance(var, bytes) + + +def is_pil_image(var): + return pil_imported and isinstance(var, Image.Image) + + +def pil_image_to_file(image, extension='JPEG', quality='web_low'): + if pil_imported: + photoBuffer = BytesIO() + image.convert('RGB').save(photoBuffer, extension, quality=quality) + photoBuffer.seek(0) + + return photoBuffer + else: + raise RuntimeError('PIL module is not imported') + + +def is_command(text: str) -> bool: + """ + Checks if `text` is a command. Telegram chat commands start with the '/' character. + :param text: Text to check. + :return: True if `text` is a command, else False. + """ + if text is None: return False + return text.startswith('/') + + +def extract_command(text: str) -> Union[str, None]: + """ + Extracts the command from `text` (minus the '/') if `text` is a command (see is_command). + If `text` is not a command, this function returns None. + + Examples: + extract_command('/help'): 'help' + extract_command('/help@BotName'): 'help' + extract_command('/search black eyed peas'): 'search' + extract_command('Good day to you'): None + + :param text: String to extract the command from + :return: the command if `text` is a command (according to is_command), else None. + """ + if text is None: return None + return text.split()[0].split('@')[0][1:] if is_command(text) else None + + +def extract_arguments(text: str) -> str: + """ + Returns the argument after the command. + + Examples: + extract_arguments("/get name"): 'name' + extract_arguments("/get"): '' + extract_arguments("/get@botName name"): 'name' + + :param text: String to extract the arguments from a command + :return: the arguments if `text` is a command (according to is_command), else None. + """ + regexp = re.compile(r"/\w*(@\w*)*\s*([\s\S]*)", re.IGNORECASE) + result = regexp.match(text) + return result.group(2) if is_command(text) else None + + +def split_string(text: str, chars_per_string: int) -> List[str]: + """ + Splits one string into multiple strings, with a maximum amount of `chars_per_string` characters per string. + This is very useful for splitting one giant message into multiples. + + :param text: The text to split + :param chars_per_string: The number of characters per line the text is split into. + :return: The splitted text as a list of strings. + """ + return [text[i:i + chars_per_string] for i in range(0, len(text), chars_per_string)] + + +def smart_split(text: str, chars_per_string: int=MAX_MESSAGE_LENGTH) -> List[str]: + r""" + Splits one string into multiple strings, with a maximum amount of `chars_per_string` characters per string. + This is very useful for splitting one giant message into multiples. + If `chars_per_string` > 4096: `chars_per_string` = 4096. + Splits by '\n', '. ' or ' ' in exactly this priority. + + :param text: The text to split + :param chars_per_string: The number of maximum characters per part the text is split to. + :return: The splitted text as a list of strings. + """ + + def _text_before_last(substr: str) -> str: + return substr.join(part.split(substr)[:-1]) + substr + + if chars_per_string > MAX_MESSAGE_LENGTH: chars_per_string = MAX_MESSAGE_LENGTH + + parts = [] + while True: + if len(text) < chars_per_string: + parts.append(text) + return parts + + part = text[:chars_per_string] + + if "\n" in part: part = _text_before_last("\n") + elif ". " in part: part = _text_before_last(". ") + elif " " in part: part = _text_before_last(" ") + + parts.append(part) + text = text[len(part):] + + +def escape(text: str) -> str: + """ + Replaces the following chars in `text` ('&' with '&', '<' with '<' and '>' with '>'). + + :param text: the text to escape + :return: the escaped text + """ + chars = {"&": "&", "<": "<", ">": ">"} + for old, new in chars.items(): text = text.replace(old, new) + return text + + +def user_link(user: types.User, include_id: bool=False) -> str: + """ + Returns an HTML user link. This is useful for reports. + Attention: Don't forget to set parse_mode to 'HTML'! + + Example: + bot.send_message(your_user_id, user_link(message.from_user) + ' started the bot!', parse_mode='HTML') + + :param user: the user (not the user_id) + :param include_id: include the user_id + :return: HTML user link + """ + name = escape(user.first_name) + return (f"{name}" + + (f" (
{user.id}
)" if include_id else "")) + + +def quick_markup(values: Dict[str, Dict[str, Any]], row_width: int=2) -> types.InlineKeyboardMarkup: + """ + Returns a reply markup from a dict in this format: {'text': kwargs} + This is useful to avoid always typing 'btn1 = InlineKeyboardButton(...)' 'btn2 = InlineKeyboardButton(...)' + + Example: + + .. code-block:: python + + quick_markup({ + 'Twitter': {'url': 'https://twitter.com'}, + 'Facebook': {'url': 'https://facebook.com'}, + 'Back': {'callback_data': 'whatever'} + }, row_width=2): + # returns an InlineKeyboardMarkup with two buttons in a row, one leading to Twitter, the other to facebook + # and a back button below + + # kwargs can be: + { + 'url': None, + 'callback_data': None, + 'switch_inline_query': None, + 'switch_inline_query_current_chat': None, + 'callback_game': None, + 'pay': None, + 'login_url': None + } + + :param values: a dict containing all buttons to create in this format: {text: kwargs} {str:} + :param row_width: int row width + :return: InlineKeyboardMarkup + """ + markup = types.InlineKeyboardMarkup(row_width=row_width) + buttons = [ + types.InlineKeyboardButton(text=text, **kwargs) + for text, kwargs in values.items() + ] + markup.add(*buttons) + return markup + + +# CREDITS TO http://stackoverflow.com/questions/12317940#answer-12320352 +def or_set(self): + self._set() + self.changed() + + +def or_clear(self): + self._clear() + self.changed() + + +def orify(e, changed_callback): + if not hasattr(e, "_set"): + e._set = e.set + if not hasattr(e, "_clear"): + e._clear = e.clear + e.changed = changed_callback + e.set = lambda: or_set(e) + e.clear = lambda: or_clear(e) + + +def OrEvent(*events): + or_event = threading.Event() + + def changed(): + bools = [ev.is_set() for ev in events] + if any(bools): + or_event.set() + else: + or_event.clear() + + def busy_wait(): + while not or_event.is_set(): + # noinspection PyProtectedMember + or_event._wait(3) + + for e in events: + orify(e, changed) + or_event._wait = or_event.wait + or_event.wait = busy_wait + changed() + return or_event + + +def per_thread(key, construct_value, reset=False): + if reset or not hasattr(thread_local, key): + value = construct_value() + setattr(thread_local, key, value) + + return getattr(thread_local, key) + + +def chunks(lst, n): + """Yield successive n-sized chunks from lst.""" + # https://stackoverflow.com/a/312464/9935473 + for i in range(0, len(lst), n): + yield lst[i:i + n] + + +def generate_random_token(): + return ''.join(random.sample(string.ascii_letters, 16)) + + +def deprecated(warn: bool=True, alternative: Optional[Callable]=None): + """ + Use this decorator to mark functions as deprecated. + When the function is used, an info (or warning if `warn` is True) is logged. + :param warn: If True a warning is logged else an info + :param alternative: The new function to use instead + """ + def decorator(function): + def wrapper(*args, **kwargs): + info = f"`{function.__name__}` is deprecated." + (f" Use `{alternative.__name__}` instead" if alternative else "") + if not warn: + logger.info(info) + else: + logger.warning(info) + return function(*args, **kwargs) + return wrapper + return decorator + + +# Cloud helpers +def webhook_google_functions(bot, request): + """A webhook endpoint for Google Cloud Functions FaaS.""" + if request.is_json: + try: + request_json = request.get_json() + update = types.Update.de_json(request_json) + bot.process_new_updates([update]) + return '' + except Exception as e: + print(e) + return 'Bot FAIL', 400 + else: + return 'Bot ON' + + +def antiflood(function, *args, **kwargs): + """ + Use this function inside loops in order to avoid getting TooManyRequests error. + Example: + + .. code-block:: python3 + + from telebot.util import antiflood + for chat_id in chat_id_list: + msg = antiflood(bot.send_message, chat_id, text) + + :param function: + :param args: + :param kwargs: + :return: None + """ + from telebot.apihelper import ApiTelegramException + from time import sleep + msg = None + try: + msg = function(*args, **kwargs) + except ApiTelegramException as ex: + if ex.error_code == 429: + sleep(ex.result_json['parameters']['retry_after']) + msg = function(*args, **kwargs) + finally: + return msg diff --git a/telebot/version.py b/telebot/version.py new file mode 100644 index 0000000..0a15dc1 --- /dev/null +++ b/telebot/version.py @@ -0,0 +1,3 @@ +# Versions should comply with PEP440. +# This line is parsed in setup.py: +__version__ = '4.4.0'