From fbb74dd6a9295d10aad486654c606acc38b00dee Mon Sep 17 00:00:00 2001 From: NormalParameter Date: Sat, 12 Mar 2022 18:31:10 +0100 Subject: [PATCH] Inserted Python API for bot and simple echo bot --- bot.py | 18 +- telebot/__init__.py | 3887 +++++++++++++++++ telebot/__pycache__/__init__.cpython-39.pyc | Bin 0 -> 146998 bytes telebot/__pycache__/apihelper.cpython-39.pyc | Bin 0 -> 46834 bytes .../__pycache__/custom_filters.cpython-39.pyc | Bin 0 -> 10023 bytes .../handler_backends.cpython-39.pyc | Bin 0 -> 8371 bytes telebot/__pycache__/types.cpython-39.pyc | Bin 0 -> 92577 bytes telebot/__pycache__/util.cpython-39.pyc | Bin 0 -> 17703 bytes telebot/apihelper.py | 1776 ++++++++ telebot/async_telebot.py | 3357 ++++++++++++++ telebot/asyncio_filters.py | 328 ++ telebot/asyncio_handler_backends.py | 56 + telebot/asyncio_helper.py | 1741 ++++++++ telebot/asyncio_storage/__init__.py | 13 + telebot/asyncio_storage/base_storage.py | 68 + telebot/asyncio_storage/memory_storage.py | 66 + telebot/asyncio_storage/pickle_storage.py | 109 + telebot/asyncio_storage/redis_storage.py | 171 + telebot/callback_data.py | 115 + telebot/custom_filters.py | 336 ++ telebot/handler_backends.py | 206 + telebot/storage/__init__.py | 13 + .../__pycache__/__init__.cpython-39.pyc | Bin 0 -> 513 bytes .../__pycache__/base_storage.cpython-39.pyc | Bin 0 -> 2683 bytes .../__pycache__/memory_storage.cpython-39.pyc | Bin 0 -> 2321 bytes .../__pycache__/pickle_storage.cpython-39.pyc | Bin 0 -> 3783 bytes .../__pycache__/redis_storage.cpython-39.pyc | Bin 0 -> 4900 bytes telebot/storage/base_storage.py | 65 + telebot/storage/memory_storage.py | 67 + telebot/storage/pickle_storage.py | 115 + telebot/storage/redis_storage.py | 180 + telebot/types.py | 2888 ++++++++++++ telebot/util.py | 512 +++ telebot/version.py | 3 + 34 files changed, 16089 insertions(+), 1 deletion(-) create mode 100644 telebot/__init__.py create mode 100644 telebot/__pycache__/__init__.cpython-39.pyc create mode 100644 telebot/__pycache__/apihelper.cpython-39.pyc create mode 100644 telebot/__pycache__/custom_filters.cpython-39.pyc create mode 100644 telebot/__pycache__/handler_backends.cpython-39.pyc create mode 100644 telebot/__pycache__/types.cpython-39.pyc create mode 100644 telebot/__pycache__/util.cpython-39.pyc create mode 100644 telebot/apihelper.py create mode 100644 telebot/async_telebot.py create mode 100644 telebot/asyncio_filters.py create mode 100644 telebot/asyncio_handler_backends.py create mode 100644 telebot/asyncio_helper.py create mode 100644 telebot/asyncio_storage/__init__.py create mode 100644 telebot/asyncio_storage/base_storage.py create mode 100644 telebot/asyncio_storage/memory_storage.py create mode 100644 telebot/asyncio_storage/pickle_storage.py create mode 100644 telebot/asyncio_storage/redis_storage.py create mode 100644 telebot/callback_data.py create mode 100644 telebot/custom_filters.py create mode 100644 telebot/handler_backends.py create mode 100644 telebot/storage/__init__.py create mode 100644 telebot/storage/__pycache__/__init__.cpython-39.pyc create mode 100644 telebot/storage/__pycache__/base_storage.cpython-39.pyc create mode 100644 telebot/storage/__pycache__/memory_storage.cpython-39.pyc create mode 100644 telebot/storage/__pycache__/pickle_storage.cpython-39.pyc create mode 100644 telebot/storage/__pycache__/redis_storage.cpython-39.pyc create mode 100644 telebot/storage/base_storage.py create mode 100644 telebot/storage/memory_storage.py create mode 100644 telebot/storage/pickle_storage.py create mode 100644 telebot/storage/redis_storage.py create mode 100644 telebot/types.py create mode 100644 telebot/util.py create mode 100644 telebot/version.py 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 0000000000000000000000000000000000000000..fa374691bce00bf408503d0006329c3940d8df0b GIT binary patch literal 146998 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

VFFWN`mI!;n^yJra)X@L}pA54P=HeETBjd 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 F{QnteY+V2V literal 0 HcmV?d00001 diff --git a/telebot/__pycache__/apihelper.cpython-39.pyc b/telebot/__pycache__/apihelper.cpython-39.pyc new file mode 100644 index 0000000000000000000000000000000000000000..31dc600615576b473c486d476f4ee7ff3151f347 GIT binary patch literal 46834 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 literal 0 HcmV?d00001 diff --git a/telebot/__pycache__/handler_backends.cpython-39.pyc b/telebot/__pycache__/handler_backends.cpython-39.pyc new file mode 100644 index 0000000000000000000000000000000000000000..d5f1b7d034951b9bd5ceb8fe97cd19b8eae13b9a GIT binary patch literal 8371 zcmb_h%abETdaswHl3LxP?im>dTySJBOKwwUN#(#;Od)Cb@hqxf52&fzptv)lDc5nHKoW@ zepy*rUw)6@_hnXZdAVWWYJPog^B?awjQ=GudsRW4N74U-$~Sy7GP>En*)@f>x)%Pe z$R0Rd$27*LK-p1cQ0-Pt!*{y1kBwo=H2Cu?-P$L{(0a~z%kZmy?Uv!!0{6h~)|e;U+jP^-Rq$>`ko4OV7sbj-Bg8-+VTGzt`m%7=j;>dSBTd3W}*L7Yd? zFQ5txN&yluySBgN+rD$l>^gqMuii4c6~E@Y_^)X_n-rKsjRhH~-u?S%_3UHM1{N?+NE z;=SO?>w(@&;?b4Sjmtq4Y^&bjwK#e8{fk!;kehLGC7;dP%+{mVM>kT}^TJ`6c-|@* z;`D~;nvOX>J-_(%VqBkcEiSfA6j==oG%*g0FR-FZhN^(PW2PrJ;^g9B6a|A|m<0YC zO2z7qt!lWNwhE56RlGm)LO-qLJ+i{As>?_fY=;oB^6~)`ofg5G~`9Cr5HxA8-b=x$JVRK?_%iW3jvbdZWY{wE8ORK%n2rjRt?FgAnyRNq%y7c&wC{yV&BC z6Po-iC%Gr92hd;odK$*lGp~Q7^!2mGcuZ$^^~X*Q817aW1vJ$3UO$LDQeJP1un@&P zA7H$ot8|(DQ*U`@hRsJy*_<1`(W$9ZT=qSzh&YS>@DTgToZ>#*XrpM}bWOJ^F2UYo zEEaT7!rfw7!rFCuO#LoUP-J)ypkB(b^ZJOReueUtsXS| z2PB@F2WEE()ESAVq?$iB9HYAompYrSy$K15-V-Shek z-GDaHn1B~Ci}``*$uL`H6~FPB<6`;l_{UXW;1oYY^+N$?VjNnxnQ0YX4ht8K8BWrc zPI@ZwWO=fv@5G@$;fuK(!z2zh&GBO;y#H_83Vy3lw9}@P(*C2Y=v_%Cbh0@&MlH@Gc_w#`fD^M z<}S8#zUA9rTE26?annvr7?jP;%a=$L5*sN)?KD94!20UNn@-~FR%A=@c2Yg8O^k^{ zoefvme?)%_fjIab*tpkv-q5?x!C?9O1O;z!59A*#v0)kXuIo`0CSuf=xrGU?BX9TOy|J?nxnWmTRWc&AV$Jb;*uE*9Ix+GsMY~8> zFMM?A@?X8@U3%lgi?6}C#_o5(mXlLojAHPl;(UYHMQ;Z^x(X3(h| z8DY215zKV?sJw<9i^_WzZ|$OJf|dcFS+QF1o)zmH0JUL_%ak~0_H&s%Ep)2DzXy@P zHW3L82y-lLM8Df6kir4&Ot2@1;$FCEC6(Q(DDIV{cIYxqy=f1d>@BEcO8azHX%kaj zO$?|Fvjey7OPS5v8N#HN+UzvcpMoJpU{>c*r8NMZ4#$D|Gj>t@>Sb1c&YKP`RlSO4 z?k?gw>U9n%16R7_E{blUGOV*Eq-Qp*r!9w4e6qyZ%e=1xhTC{vJ%S1z3oBATWz)Re zSy{}-8XU;i&1_N#gc)+uE7K;xE(!aCAlZq1MO`a0nl9)0KtG70A}0_?eTqW_ub7BD zk0Kj&*IcQ(HP>+~uIt*3nq@XmSf(T^eTF3aVdJeRBF zxm-2R|bJFt^~X5?{VGUqV!LcH?lE@nq{nVOL4cyC>{W4Hiz@m@-xr>-^9!2x{DY_y>`jsh1 zNKV%d9HDr=>tMf3->jvTONa!)2KIu;8JPAROM9te@gm}rkEAy{jV;bH9VQs6EVJeK zU!W95vLdfKf)eIO#l<#@yoidTF%-KPF1G9qDbD49B%b$63@F~RT)ai@z(5F0Gp7~g zO8pJ1fYp)Ur6q#@3^)D-MfPbByrtMfF6J&9)Oi%momjcZMaY$ONqEw4@kDn+^)P%+ z%A9EG4R3sFB$tz?#RK_!udl+p0j0y>OK0V&mT~erv>SbqxFNK^f(s~A%1Y1eS$Yd$~K6ni>r*(RP_^z3YqSy;Ar|9~xeo!UT^R^!dx zpr6R6IE6i;ksYnS!`5f47)2CaS8PCW`Z7*Z_vP$s;VZOa{U|DY(=c5ek6;%pTd9WK SI@wxnz1@0v#l^3+()=C8K-Vb% literal 0 HcmV?d00001 diff --git a/telebot/__pycache__/types.cpython-39.pyc b/telebot/__pycache__/types.cpython-39.pyc new file mode 100644 index 0000000000000000000000000000000000000000..4f3ed0a7b8763e341ef16d5c83d5fed96a2bcac4 GIT binary patch literal 92577 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 literal 0 HcmV?d00001 diff --git a/telebot/__pycache__/util.cpython-39.pyc b/telebot/__pycache__/util.cpython-39.pyc new file mode 100644 index 0000000000000000000000000000000000000000..6fb78f8c53a1bc5772b3655678f3e66a4ad35ba5 GIT binary patch literal 17703 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 TikexxKUgfFMdrYCVekI}B%nTp literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..ed5cd05a31533ce49e948c9ac97e57e87de30b5b GIT binary patch literal 513 zcmZWmOHRWu6twfFYNf&&*hfM+073{_HY`9Dg^gv&O39;&YS)o%Bzgp{!LIk<6xp)k z3aqf*P*8D{(VOSVym?AF81#@m{r8jED@N$Uk-sOfFZi%qHAQd_)(vO&KdWXKamBX1Ry2@%WTJ zXi%Cyak&6`1$tp*Lz~sT-C?duF_PwTa!tD-wA+C)RxfMASkbK7>`ch&nZvpL#Yj8z JK6cMA`39C8k~9DS literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..4860da456144c0a2efd04b13189ecba97259c58c GIT binary patch literal 2683 zcmb7GOK&4Z5bk-8od?+syO3QL8nlNs3PD1G1F$G;7Daop&;~A+rPgG+lh_%1Om};U zEuSDigF_;5$^YP2%#{;=ffH3d4^Qj_LR(Y)n3}GyzpAcsw6@kH&{qH0KK$J$F!Nw$bL)V#{J%h(v}2U@R_*p0Rt4!; zoZuJS{>|bJchB99!#(bwla9*+9zyT&25&;|^A)}deZbfFI`kobi*G>R;P?1_=$ppw zX0~ylBP|YeDx+iZB2uD#Wo5n;dMt;rRl|Z~eD>f`E(lHz$d%Q;0+G2&Bu5hXAV&PX zzxQSLM6pvNM`w zA!EH^Pcw!Vl?{Se)2&6!JEd0ap+?bgTpAT;HXj_6S;X++0> zn)Ze;8bwm~;&Bp58lOaZr}_dJ+H9x#)nJr}ff#DRzmqbREl;AAEI@VE<5_^4;lMc+ zlgxV)C1WusNj6Y9R6;YfAHXa>pnJmsORX3qqHH++?*s1t{{zYu*!jva$B}C=mg^`g z{}#}X3+SkEJM>a3Mi-V1$G1VxDyGpl3gfeIPR%7q?M}1O{E&h}1l7Pioup%`PSSD0 zX)FbP9nnfria3>=_Ks)|{GJL$hpDFGtf%xgjfQ-#2`UjS)PL^jlLASUz@B`Pf1FCrfDSns)~LyaN1Mz;Ww0b|EP8%e!!6 zZwKC#xSzR!PiwFrFTtMc-2pVDMv3X?cDoI{7KZhYE3?)OzOB^maDA>>T@ErA)6Amd zTot_$pVS~cSUNdsv{o<`52Jx#ENe10NclLy@hW3KjiV%gBe6EhdnhnsFJhEZ=$?o@wsO;)L@eiHWytIIa>g9%)AU6-rAQELfTr3}y|+ zu<}X9p2OR+GC!l}Ky`mhpzP90j3a?(1q0MXDfwscy zya8>MZ*Ujd8gKGVXdV6zZ$VqMDutfQ z*_guldBHw|dH_`+RUwIq4@?ZmlJ=K0v3Z~aSZpgXIbITpI{!XD_-gcv5>kzhC()T0 zeJj+Nj?PEtmxp2^PGm6MkMuV`Js)Wp9z}YjQkF(jgUQc^z4Ob&@%?ZXYTrjsD1<`j z;^9pLdb!>h74w39tOouJI-10mrdLe1V(XN>v|<{QQ|p4rUt@;7m~IgmJ&mn=6tgXG z`!2Y80Bb{V_N$d<%(Cw+p9#%=ai;It{iLEW3=Taen{d8#KzMf22vsfsGSw;V>QwJ19S?YcXEhXbnnD={v{8E*h(+G zhTQ)e69{|5uIQhTt_y}NKFh9d_m7v|V8*-I1;!zdgzC;B-4!oFr9C_K(>(x^xIfQl zI-H8{q>N-zJ&xow&|YQjt|Zo(xJ)V+!DKGh1JKDydJOv&NV<AW&&9_GbF}X#4d0(ed_KXx z>-)dYgGo9gku7-_1!gVzE{gY1JVen&@ga(jP`t%rdYH~7@{V>YOp6ZPtU67{b!yqq rGV^_SLLOz0XD@#(kKjK>{&Y%YI`t6lS9Vb1?4~9>#Yf&)-(vp(TV3+$ literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..0bd6f63eb026e9198efa8750e0848fb131a199e6 GIT binary patch literal 3783 zcmb_f&2rmD62=Tb5~3)Xk+n_~D>jl;Hp@y($N7&}QSC-{VppYTuch78va~@#oRI|y zBI^3}C3kb`v8VXO8b_9<}9DX-vDz8;ViMbWo_h55t$bobZ&^)Q&5D_eLf z|Nn67zn3iQ7wU{Z4my9qn|z8wSb{}Xn>AURH#ws|kL;#xYNzRZX9-(4&n@9dc4RjT zg0EY(!mn6q)p%B2*MXMnI#$7B`Av|>tYSvi;*OS2b)!Y|#-GvIDMqE$WWs84aZPYx z|ICCV3eO8TyC_P}t)?SfF@w4w%3>CEQB=en>XMijmr%Q+Dqcf9BNoJE)Mc?K{(yQ` zToJFMu823p66!hernriFUM!2ZP+u}ST+bGas1L)|UNltUz^@Mb~kp%nlrfDYi z?q1ZWv8<@1?x{|Tk7-10de`tK%P3NoTE3N9$Lz#nRPz&*J`B2Uow7#F$!500Nc!DC z?_}kck~qv4p~@UIMdpY=2e7P@$Y@)+*znX+bEsb4U%S2e_e82>b32On z*?qPlBl%bb?QdfJ*N68vHM(1|-b`{>HoHbcf6%RWpJlG^hn-OSJ~>KKL}9Tab6NlD zMU&Krb6bq&4IaXgUZeKsXoS`w6yG&!-?9F+xXBOsp`G&74jD|sSF9)VBh$bn@H7Tb z;{E@Xy}<+JJiQYp-gd9k(qY{3bnLa_&c0OIizDF;G@#**JQ#NCqYXCBEvAiP<#lD; zjih(56MF|i;`NfS^VrKxGTdRbf!+zUnH6?CoSKjfD_?(F|D=BF?36~$R%TVzlZz|Qp<`nkavg(Cga#qA%LJo1d%=aX1hh2YwNoYvqWA)FkDhZ}pm`@O z+0jL6iP91O&+;MHrDHd=QhtJYKUl(b0R558tWNcomHhY-u%zEV2~!uC&h$SXS#-_# zw+H{0Ex6+|uTF7wV<+f{2nIg0>PvR>S8_0fncEJYCfz91iUu}lL_Q4@?ZG91~c@Frg^RoVY7HSuw{O!PRSb0Uu$6j+su=CXi7x`2G&w z913TV&6{8Ut!YqgdOsf9Xp06h*o(c<7%r`P}ecv z=%_rjGAD|`424z{CvqUYdY7h<8&1kj-t!^egdD(P6;`IeetnAM#5d<;CLSx`H8Rs4 z=VgYwSi4S2j)OGt0uBJ_izbx8Z8f{mw-2^HsIjKoRk4m+aa1c!sG{D+u14%;_1^vO z?%iGA@E<;Uu<>B+!T0Kq_*UZvoLoJky|Z{xuc7-f-T`4%*dps+o1)iPJhq6VhR-?9 z+Q%r=vo!!w)zsc4wju_z_bv4g@D^GrTe8qwO-XUnOZgIbYXv!S9cx46#-B$siQ~*T zah~}C<}>S70DTU#$vjRYD;aB$O&IMEtwVLtzCpn=5&I#%z-BT z2LzLqTA(UF6j`ZLQ(RqtyGl|sTeH`@#)+Sd_Sn!7;-g&mb9KOgLSP}lQJjITq z#VvB=;X`eDjg#C%13H5B$(_|5YLnH_=i~j9e@%&z?{W2IJ_mi18uL70>|!@IzC9gq ztNmDN12_gKTGaP>X`m4k) zD@m?Qg8xW@lRxGDu>kLI*i>3SThk&+96r<@QRE|Z#>8J83On%r)zle|41FWD4HxA1 zAE=;hVIPB1DYqW6f8#bhrc(b;gCO^QI`oh*ad$DHPF)ep#OuV`Gno-7!yspzjA$aO zJnD6H*p_#did9y`MW`KUOq+pgbmE2;};2y5H0QcygNQYBO zA$^Yko+@FW#uU-XREJw67YgxLLIt=G?&o)`4oO6*%3}TZr$qI%H5c;0h{3L5AH(m4 zx5nJeT;B-~MSqjP)W*VN96Uxz=F@})9lGc|!P3O!Akgj9Buukl0`jR^l`iJQ_r9N% zeZL)x9{wo!S;hCC^nxh=qUesO7O9|XilTgPm{Q%K)-5VNq2e9NhcS}|K5{Kvq5CG*#>zJ>pOU@_BC g1Wpd>Rcd)u&;ip<&fJw;&sDnYnbclouI<{t0is(+TmS$7 literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..69ad714a4c3b17a79b135ffb4e132b4eddafc517 GIT binary patch literal 4900 zcmcgwTW{RP73T0J?rL>Q6g#pNhfR&x8@Q^|xM`{=YHB%l4YWdFsVK5gv7k7VMTxuI z3}Kl>Ovbfo@ z2g`VCMKc|z<#cMT8qgD||<5 zveep&yd-Jr>Ee#`WUwP+;XMuRdr6Qs)RV2aD}(29dlzG@Cjk$WbRN&TfiJ_j`)M3U z4}DszdRgdvj9)T<2(%XCT1)4wW$1@D+Xy_c?x@ui`$>-{3cJU*KXR$bT<8E}Sh9(46l8=eqeiFE$ zyyG7A6Y2J7Nm;Hg6P zSYSVvZJ)R@x~NURE&P((5KBiCxyHGib0>0)x&YDozJqbPcRxj+0Ub}+majR(w z+EQw%Zff^La1cIE7gV=hES1(idF}^2Vo}ph%_N8p#4P5C28lTm^CT7_E;O->+t)wY zTHpJ55{P8)Ac~KJy$3;ZEaTo@?{pXFJQZFC5dZko$9p+gdpX*B*|$f=?VYAh8#@VQ zyZtKGO&kb~y<*zTWP@uHrdb;e_H`PVUncIvbcOvo8o*vVV!)lf(1-d<9X&&uLw3fl zYUk{QfoIk^d~;}s_oe-EW@wzV9r!m{WAGbge4^&VepmUHD`WR*Ac3k6i?|0O?zr5O z-aec&;~tHEEZsv-S?MtBKBYN4cAtgv5L7N%N+W;f@j%gc-}8@yE_eO7)AMAwA4Z`( zEk1w2S_^*R;O!nfe7org(o`%!qy{#ntdbgu6shG$F`|Wel*HZCieir^O;Zr8sc{^f zruA~M)D}U~kEAkYCSId)b*$LV)LcbBQL)l)uw~}3c{W&`G)OtYH!x##kx2@7xQ9-~ zB*xGfH;J@f+C%-Eo$F+hmosFV+!((u(5MN% zwRpeNOT;y_bL7Pv>iLNm^#gH(29ojWb=mH8hS)#2Hik6gj{=(ZmL2 zf}y^Gief|iyfrjJQ2bALH`F(@?mT6R>`93Au(E~hVUk0rD#09V6b9M>`Bg+wA;d$T z6EgJsktZns3FH9RbNxe4mZ2%N)P$xkZeic4NglJ?w1tbFqHkIc6O|9TevsPn{t=3b z)a-hlK-|O_BoY-yW@y=V$kQ465*pch4uVN9?&8rUa@UBh>eHK8B|*If2Q-!*mn#40VJ^&xTiJ5|e-u8uFf|YZv0uhDKsW{ar`Ka&t7?MztlF^Gm zTEyQpT9tDMiP1%rqW%_s3E8ATrb?@Ka$3DZv?5oopm28#g^Vx%T6OZpZSg%wahJp; z7AWo1Iz7Bis2D4?o9;5Co0>7P$7{dTz+|5L6yTWsF2*$YTL&!94Q5TPXh|Kul8Q#=k-8-R%pbWVQ1C zsb;Ptc;=e_mNXw{aRW|1tqdq9`IMJN7nNLv_G1oi717aB4sPWRZHofegY@PwmptIgr1~@*xx%6KY9!R@T+oT7xkRT* zs$SadwBBxaV&0Fa-DtNz?|V`HW~SZdvEObBs!K9gy-6*Vt-njHcS*cQ;vR|bllTFN z4@mrw#D^qGYpNCYa26nE)X3&+%kG`}(Y_F5 z@pLsmH?NIO%5>I5Y*+sj5=3f6EOqPT}Udp!`$dnGs MF`X^zHmmFZ1P>f^wEzGB literal 0 HcmV?d00001 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'