diff --git a/README.md b/README.md index 2ea415f..fbe40e8 100644 --- a/README.md +++ b/README.md @@ -584,5 +584,9 @@ Get help. Discuss. Chat. * [SmartySBot](http://t.me/ZDU_bot)([link](https://github.com/0xVK/SmartySBot)) by *0xVK* - Telegram timetable bot, for Zhytomyr Ivan Franko State University students. * [yandex_music_bot](http://t.me/yandex_music_bot)- Downloads tracks/albums/public playlists from Yandex.Music streaming service for free. * [LearnIt](https://t.me/LearnItbot)([link](https://github.com/tiagonapoli/LearnIt)) - A Telegram Bot created to help people to memorize other languages’ vocabulary. +* [MusicQuiz_bot](https://t.me/MusicQuiz_bot) by [Etoneja](https://github.com/Etoneja) - Listen to audiosamles and try to name the performer of the song. +* [Bot-Telegram-Shodan ](https://github.com/rubenleon/Bot-Telegram-Shodan) by [rubenleon](https://github.com/rubenleon) +* [MandangoBot](https://t.me/MandangoBot) by @Alvaricias - Bot for managing Marvel Strike Force alliances (only in spanish, atm). + Want to have your bot listed here? Send a Telegram message to @eternnoir or @pevdh. diff --git a/README.rst b/README.rst deleted file mode 100644 index d776571..0000000 --- a/README.rst +++ /dev/null @@ -1,807 +0,0 @@ -# - -.. raw:: html - -

- -pyTelegramBotAPI - -.. raw:: html - -

- -A simple, but extensible Python implementation for the `Telegram Bot -API `__. - -|Download Month| |Build Status| |Download Month| - -- `Getting started. <#getting-started>`__ -- `Writing your first bot <#writing-your-first-bot>`__ - - - `Prerequisites <#prerequisites>`__ - - `A simple echo bot <#a-simple-echo-bot>`__ - -- `General API Documentation <#general-api-documentation>`__ - - - `Types <#types>`__ - - `Methods <#methods>`__ - - `General use of the API <#general-use-of-the-api>`__ - - `Message handlers <#message-handlers>`__ - - `Callback Query handlers <#callback-query-handler>`__ - - `TeleBot <#telebot>`__ - - `Reply markup <#reply-markup>`__ - - `Inline Mode <#inline-mode>`__ - -- `Advanced use of the API <#advanced-use-of-the-api>`__ - - - `Asynchronous delivery of - messages <#asynchronous-delivery-of-messages>`__ - - `Sending large text messages <#sending-large-text-messages>`__ - - `Controlling the amount of Threads used by - TeleBot <#controlling-the-amount-of-threads-used-by-telebot>`__ - - `The listener mechanism <#the-listener-mechanism>`__ - - `Using web hooks <#using-web-hooks>`__ - - `Logging <#logging>`__ - -- `F.A.Q. <#faq>`__ - - - `Bot 2.0 <#bot-20>`__ - - `How can I distinguish a User and a GroupChat in - message.chat? <#how-can-i-distinguish-a-user-and-a-groupchat-in-messagechat>`__ - -- `The Telegram Chat Group <#the-telegram-chat-group>`__ -- `More examples <#more-examples>`__ -- `Bots using this API <#bots-using-this-api>`__ - -Getting started. ----------------- - -This API is tested with Python 2.6, Python 2.7, Python 3.4, Pypy and -Pypy 3. There are two ways to install the library: - -- Installation using pip (a Python package manager)\*: - -:: - - $ pip install pyTelegramBotAPI - -- Installation from source (requires git): - -:: - - $ git clone https://github.com/eternnoir/pyTelegramBotAPI.git - $ cd pyTelegramBotAPI - $ python setup.py install - -It is generally recommended to use the first option. - -\*\*While the API is production-ready, it is still under development and -it has regular updates, do not forget to update it regularly by calling -``pip install pytelegrambotapi --upgrade``\ \* - -Writing your first bot ----------------------- - -Prerequisites -~~~~~~~~~~~~~ - -It is presumed that you [have obtained an API token with -@BotFather](https://core.telegram.org/bots#botfather). We will call this -token ``TOKEN``. Furthermore, you have basic knowledge of the Python -programming language and more importantly `the Telegram Bot -API `__. - -A simple echo bot -~~~~~~~~~~~~~~~~~ - -The TeleBot class (defined in \_\_init\_\_.py) encapsulates all API -calls in a single class. It provides functions such as ``send_xyz`` -(``send_message``, ``send_document`` etc.) and several ways to listen -for incoming messages. - -Create a file called ``echo_bot.py``. Then, open the file and create an -instance of the TeleBot class. - -.. code:: python - - import telebot - - bot = telebot.TeleBot("TOKEN") - -*Note: Make sure to actually replace TOKEN with your own API token.* - -After that declaration, we need to register some so-called message -handlers. Message handlers define filters which a message must pass. If -a message passes the filter, the decorated function is called and the -incoming message is passed as an argument. - -Let's define a message handler which handles incoming ``/start`` and -``/help`` commands. - -.. code:: python - - @bot.message_handler(commands=['start', 'help']) - def send_welcome(message): - bot.reply_to(message, "Howdy, how are you doing?") - -A function which is decorated by a message handler **can have an -arbitrary name, however, it must have only one parameter (the -message)**. - -Let's add another handler: - -.. code:: python - - @bot.message_handler(func=lambda m: True) - def echo_all(message): - bot.reply_to(message, message.text) - -This one echoes all incoming text messages back to the sender. It uses a -lambda function to test a message. If the lambda returns True, the -message is handled by the decorated function. Since we want all messages -to be handled by this function, we simply always return True. - -*Note: all handlers are tested in the order in which they were declared* - -We now have a basic bot which replies a static message to "/start" and -"/help" commands and which echoes the rest of the sent messages. To -start the bot, add the following to our source file: - -.. code:: python - - bot.polling() - -Alright, that's it! Our source file now looks like this: - -.. code:: python - - import telebot - - bot = telebot.TeleBot("TOKEN") - - @bot.message_handler(commands=['start', 'help']) - def send_welcome(message): - bot.reply_to(message, "Howdy, how are you doing?") - - @bot.message_handler(func=lambda message: True) - def echo_all(message): - bot.reply_to(message, message.text) - - bot.polling() - -To start the bot, simply open up a terminal and enter -``python echo_bot.py`` to run the bot! Test it by sending commands -('/start' and '/help') and arbitrary text messages. - -General API Documentation -------------------------- - -Types -~~~~~ - -All types are defined in types.py. They are all completely in line with -the `Telegram API's definition of the -types `__, except -for the Message's ``from`` field, which is renamed to ``from_user`` -(because ``from`` is a Python reserved token). Thus, attributes such as -``message_id`` can be accessed directly with ``message.message_id``. -Note that ``message.chat`` can be either an instance of ``User`` or -``GroupChat`` (see `How can I distinguish a User and a GroupChat in -message.chat? <#how-can-i-distinguish-a-user-and-a-groupchat-in-messagechat>`__). - -The Message object also has a ``content_types``\ attribute, which -defines the type of the Message. ``content_types`` can be one of the -following strings: 'text', 'audio', 'document', 'photo', 'sticker', -'video', 'voice', 'location', 'contact', 'new\_chat\_participant', -'left\_chat\_participant', 'new\_chat\_title', 'new\_chat\_photo', -'delete\_chat\_photo', 'group\_chat\_created'. - -Methods -~~~~~~~ - -All `API -methods `__ are -located in the TeleBot class. They are renamed to follow common Python -naming conventions. E.g. ``getMe`` is renamed to ``get_me`` and -``sendMessage`` to ``send_message``. - -General use of the API -~~~~~~~~~~~~~~~~~~~~~~ - -Outlined below are some general use cases of the API. - -Message handlers -^^^^^^^^^^^^^^^^ - -A message handler is a function that is decorated with the -``message_handler`` decorator of a TeleBot instance. Message handlers -consist of one or multiple filters. Each filter much return True for a -certain message in order for a message handler to become eligible to -handle that message. A message handler is declared in the following way -(provided ``bot`` is an instance of TeleBot): - -.. code:: python - - @bot.message_handler(filters) - def function_name(message): - bot.reply_to(message, "This is a message handler") - -``function_name`` is not bound to any restrictions. Any function name is -permitted with message handlers. The function must accept at most one -argument, which will be the message that the function must handle. -``filters`` is a list of keyword arguments. A filter is declared in the -following manner: ``name=argument``. One handler may have multiple -filters. TeleBot supports the following filters: - -+--------+------+------+ -| name | argu | Cond | -| | ment | itio | -| | (s) | n | -+========+======+======+ -| conten | list | ``Tr | -| t\_typ | of | ue`` | -| es | stri | if | -| | ngs | mess | -| | (def | age. | -| | ault | cont | -| | ``[' | ent\ | -| | text | _typ | -| | ']`` | e | -| | ) | is | -| | | in | -| | | the | -| | | list | -| | | of | -| | | stri | -| | | ngs. | -+--------+------+------+ -| regexp | a | ``Tr | -| | regu | ue`` | -| | lar | if | -| | expr | ``re | -| | essi | .sea | -| | on | rch( | -| | as a | rege | -| | stri | xp_a | -| | ng | rg)` | -| | | ` | -| | | retu | -| | | rns | -| | | ``Tr | -| | | ue`` | -| | | and | -| | | ``me | -| | | ssag | -| | | e.co | -| | | nten | -| | | t_ty | -| | | pe = | -| | | = 't | -| | | ext' | -| | | `` | -| | | (See | -| | | `Pyt | -| | | hon | -| | | Regu | -| | | lar | -| | | Expr | -| | | essi | -| | | ons | -| | | ` | -| | | __ | -+--------+------+------+ -| comman | list | ``Tr | -| ds | of | ue`` | -| | stri | if | -| | ngs | ``me | -| | | ssag | -| | | e.co | -| | | nten | -| | | t_ty | -| | | pe = | -| | | = 't | -| | | ext' | -| | | `` | -| | | and | -| | | ``me | -| | | ssag | -| | | e.te | -| | | xt`` | -| | | star | -| | | ts | -| | | with | -| | | a | -| | | comm | -| | | and | -| | | that | -| | | is | -| | | in | -| | | the | -| | | list | -| | | of | -| | | stri | -| | | ngs. | -+--------+------+------+ -| func | a | ``Tr | -| | func | ue`` | -| | tion | if | -| | (lam | the | -| | bda | lamb | -| | or | da | -| | func | or | -| | tion | func | -| | refe | tion | -| | renc | refe | -| | e) | renc | -| | | e | -| | | retu | -| | | rns | -| | | ``Tr | -| | | ue`` | -+--------+------+------+ - -Here are some examples of using the filters and message handlers: - -.. code:: python - - import telebot - bot = telebot.TeleBot("TOKEN") - - # Handles all text messages that contains the commands '/start' or '/help'. - @bot.message_handler(commands=['start', 'help']) - def handle_start_help(message): - pass - - # Handles all sent documents and audio files - @bot.message_handler(content_types=['document', 'audio']) - def handle_docs_audio(message): - pass - - # Handles all text messages that match the regular expression - @bot.message_handler(regexp="SOME_REGEXP") - def handle_message(message): - pass - - #Handles all messages for which the lambda returns True - @bot.message_handler(func=lambda message: message.document.mime_type == 'text/plain', content_types=['document']) - def handle_text_doc(message): - pass - - #Which could also be defined as: - def test_message(message): - return message.document.mime_type == 'text/plan' - - @bot.message_handler(func=test_message, content_types=['document']) - def handle_text_doc(message) - pass - - # Handlers can be stacked to create a function which will be called if either message_handler is eligible - # This handler will be called if the message starts with '/hello' OR is some emoji - @bot.message_handler(commands=['hello']) - @bot.message_handler(func=lambda msg: msg.text.encode("utf-8") == SOME_FANCY_EMOJI) - def send_something(message): - pass - -**Important: all handlers are tested in the order in which they were -declared** - -Callback Query Handler -^^^^^^^^^^^^^^^^^^^^^^ - -In bot2.0 update. You can get ``callback_query`` in update object. In -telebot use ``callback_query_handler`` to process callback\_querys. - -.. code:: python - - @bot.callback_query_handler(func=lambda call: True) - def test_callback(call): - logger.info(call) - -TeleBot -^^^^^^^ - -.. code:: python - - import telebot - - TOKEN = '' - tb = telebot.TeleBot(TOKEN) #create a new Telegram Bot object - - # Upon calling this function, TeleBot starts polling the Telegram servers for new messages. - # - none_stop: True/False (default False) - Don't stop polling when receiving an error from the Telegram servers - # - interval: True/False (default False) - The interval between polling requests - # Note: Editing this parameter harms the bot's response time - # - timeout: integer (default 20) - Timeout in seconds for long polling. - tb.polling(none_stop=False, interval=0, timeout=20) - - # getMe - user = tb.get_me() - - # setWebhook - tb.set_webhook(url="http://example.com", certificate=open('mycert.pem')) - # unset webhook - tb.remove_webhook() - - # getUpdates - updates = tb.get_updates() - updates = tb.get_updates(1234,100,20) #get_Updates(offset, limit, timeout): - - # sendMessage - tb.send_message(chatid, text) - - # forwardMessage - tb.forward_message(to_chat_id, from_chat_id, message_id) - - # All send_xyz functions which can take a file as an argument, can also take a file_id instead of a file. - # sendPhoto - photo = open('/tmp/photo.png', 'rb') - tb.send_photo(chat_id, photo) - tb.send_photo(chat_id, "FILEID") - - # sendAudio - audio = open('/tmp/audio.mp3', 'rb') - tb.send_audio(chat_id, audio) - tb.send_audio(chat_id, "FILEID") - - ## sendAudio with duration, performer and title. - tb.send_audio(CHAT_ID, file_data, 1, 'eternnoir', 'pyTelegram') - - # sendVoice - voice = open('/tmp/voice.ogg', 'rb') - tb.send_voice(chat_id, voice) - tb.send_voice(chat_id, "FILEID") - - # sendDocument - doc = open('/tmp/file.txt', 'rb') - tb.send_document(chat_id, doc) - tb.send_document(chat_id, "FILEID") - - # sendSticker - sti = open('/tmp/sti.webp', 'rb') - tb.send_sticker(chat_id, sti) - tb.send_sticker(chat_id, "FILEID") - - # sendVideo - video = open('/tmp/video.mp4', 'rb') - tb.send_video(chat_id, video) - tb.send_video(chat_id, "FILEID") - - # sendLocation - tb.send_location(chat_id, lat, lon) - - # sendChatAction - # action_string can be one of the following strings: 'typing', 'upload_photo', 'record_video', 'upload_video', - # 'record_audio', 'upload_audio', 'upload_document' or 'find_location'. - tb.send_chat_action(chat_id, action_string) - - # getFile - # Downloading a file is straightforward - # Returns a File object - import requests - file_info = tb.get_file(file_id) - - file = requests.get('https://api.telegram.org/file/bot{0}/{1}'.format(API_TOKEN, file_info.file_path)) - -Reply markup -^^^^^^^^^^^^ - -All ``send_xyz`` functions of TeleBot take an optional ``reply_markup`` -argument. This argument must be an instance of ``ReplyKeyboardMarkup``, -``ReplyKeyboardRemove`` or ``ForceReply``, which are defined in types.py. - -.. code:: python - - from telebot import types - - # Using the ReplyKeyboardMarkup class - # It's constructor can take the following optional arguments: - # - resize_keyboard: True/False (default False) - # - one_time_keyboard: True/False (default False) - # - selective: True/False (default False) - # - row_width: integer (default 3) - # row_width is used in combination with the add() function. - # It defines how many buttons are fit on each row before continuing on the next row. - markup = types.ReplyKeyboardMarkup(row_width=2) - itembtn1 = types.KeyboardButton('a') - itembtn2 = types.KeyboardButton('v') - itembtn3 = types.KeyboardButton('d') - markup.add(itembtn1, itembtn2, itembtn3) - tb.send_message(chat_id, "Choose one letter:", reply_markup=markup) - - # or add strings one row at a time: - markup = types.ReplyKeyboardMarkup() - itembtna = types.KeyboardButton('a') - itembtnv = types.KeyboardButton('v') - itembtnc = types.KeyboardButton('c') - itembtnd = types.KeyboardButton('d') - itembtne = types.KeyboardButton('e') - markup.row(itembtna, itembtnv) - markup.row(itembtnc, itembtnd, itembtne) - tb.send_message(chat_id, "Choose one letter:", reply_markup=markup) - -The last example yields this result: - -.. figure:: https://pp.vk.me/c624430/v624430512/473e5/_mxxW7FPe4U.jpg - :alt: ReplyKeyboardMarkup - - ReplyKeyboardMarkup - -.. code:: python - - # ReplyKeyboardRemove: hides a previously sent ReplyKeyboardMarkup - # Takes an optional selective argument (True/False, default False) - markup = types.ReplyKeyboardRemove(selective=False) - tb.send_message(chat_id, message, reply_markup=markup) - -.. code:: python - - # ForceReply: forces a user to reply to a message - # Takes an optional selective argument (True/False, default False) - markup = types.ForceReply(selective=False) - tb.send_message(chat_id, "Send me another word:", reply_markup=markup) - -ForceReply: - -.. figure:: https://pp.vk.me/c624430/v624430512/473ec/602byyWUHcs.jpg - :alt: ForceReply - - ForceReply - -Inline Mode -~~~~~~~~~~~ - -More information about `Inline -mode `__. - -inline\_handler -^^^^^^^^^^^^^^^ - -Now, you can use inline\_handler to get inline\_query in telebot. - -.. code:: python - - - @bot.inline_handler(lambda query: query.query == 'text') - def query_text(inline_query): - # Query message is text - -chosen\_inline\_handler -^^^^^^^^^^^^^^^^^^^^^^^ - -Use chosen\_inline\_handler to get chosen\_inline\_result in telebot. -Don't forgot add the /setinlinefeedback command for @Botfather. - -More information : -`collecting-feedback `__ - -.. code:: python - - @bot.chosen_inline_handler(func=lambda chosen_inline_result: True) - def test_chosen(chosen_inline_result): - # Process all chosen_inline_result. - -answer\_inline\_query -^^^^^^^^^^^^^^^^^^^^^ - -.. code:: python - - @bot.inline_handler(lambda query: query.query == 'text') - def query_text(inline_query): - try: - r = types.InlineQueryResultArticle('1', 'Result', 'Result message.') - r2 = types.InlineQueryResultArticle('2', 'Result2', 'Result message2.') - bot.answer_inline_query(inline_query.id, [r, r2]) - except Exception as e: - print(e) - -Advanced use of the API ------------------------ - -Asynchronous delivery of messages -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -There exists an implementation of TeleBot which executes all -``send_xyz`` and the ``get_me`` functions asynchronously. This can speed -up you bot **significantly**, but it has unwanted side effects if used -without caution. To enable this behaviour, create an instance of -AsyncTeleBot instead of TeleBot. - -.. code:: python - - tb = telebot.AsyncTeleBot("TOKEN") - -Now, every function that calls the Telegram API is executed in a -separate Thread. The functions are modified to return an AsyncTask -instance (defined in util.py). Using AsyncTeleBot allows you to do the -following: - -.. code:: python - - import telebot - - tb = telebot.AsyncTeleBot("TOKEN") - task = tb.get_me() # Execute an API call - # Do some other operations... - a = 0 - for a in range(100): - a += 10 - - result = task.wait() # Get the result of the execution - -*Note: if you execute send\_xyz functions after eachother without -calling wait(), the order in which messages are delivered might be -wrong.* - -Sending large text messages -~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -Sometimes you must send messages that exceed 5000 characters. The -Telegram API can not handle that many characters in one request, so we -need to split the message in multiples. Here is how to do that using the -API: - -.. code:: python - - from telebot import util - large_text = open("large_text.txt", "rb").read() - - # Split the text each 3000 characters. - # split_string returns a list with the splitted text. - splitted_text = util.split_string(large_text, 3000) - for text in splitted_text: - tb.send_message(chat_id, text) - -Controlling the amount of Threads used by TeleBot -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -The TeleBot constructor takes the following optional arguments: - -- create\_threads: True/False (default True). A flag to indicate - whether TeleBot should execute message handlers on it's polling - Thread. -- num\_threads: integer (default 4). Controls the amount of - WorkerThreads created for the internal thread pool that TeleBot uses - to execute message handlers. Is not used when create\_threads is - False. - -The listener mechanism -~~~~~~~~~~~~~~~~~~~~~~ - -As an alternative to the message handlers, one can also register a -function as a listener to TeleBot. Example: - -.. code:: python - - def handle_messages(messages): - for message in messsages: - # Do something with the message - bot.reply_to(message, 'Hi') - - bot.set_update_listener(handle_messages) - bot.polling() - -Using webhooks -~~~~~~~~~~~~~~ - -When using webhooks telegram sends one Update per call, for processing -it you should call process\_new\_messages([update.message]) when you -recieve it. - -There are some examples using webhooks in the -*examples/webhook\_examples* directory. - -Logging -~~~~~~~ - -You can use the Telebot module logger to log debug info about Telebot. -Use ``telebot.logger`` to get the logger of the TeleBot module. It is -possible to add custom logging Handlers to the logger. Refer to the -`Python logging module -page `__ for more info. - -.. code:: python - - import logging - - logger = telebot.logger - telebot.logger.setLevel(logging.DEBUG) # Outputs debug messages to console. - -F.A.Q. ------- - -Bot 2.0 -~~~~~~~ - -April 9,2016 Telegram release new bot 2.0 API, which has a drastic -revision especially for the change of method's interface.If you want to -update to the latest version, please make sure you've switched bot's -code to bot 2.0 method interface. - -`More information about pyTelegramBotAPI support -bot2.0 `__ - -How can I distinguish a User and a GroupChat in message.chat? -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -Telegram Bot API support new type Chat for message.chat. - -- Check the ``type`` attribute in ``Chat`` object: -- \`\`\`python if message.chat.type == “private”: # private chat - message - -if message.chat.type == “group”: # group chat message - -if message.chat.type == “supergroup”: # supergroup chat message - -if message.chat.type == “channel”: # channel message - -\`\`\` - -The Telegram Chat Group ------------------------ - -Get help. Discuss. Chat. - -- Join the pyTelegramBotAPI Telegram Chat Group -- Messge to @eternnoir by telegram for Invitation. -- We now have a Telegram Channel as well! Keep yourself up to date with - API changes, and `join it `__. - -More examples -------------- - -- `Echo - Bot `__ -- `Deep - Linking `__ -- `next\_step\_handler - Example `__ - -Bots using this API -------------------- - -- `SiteAlert bot `__ - (`source `__) by - *ilteoood* - Monitors websites and sends a notification on changes -- `TelegramLoggingBot `__ - by *aRandomStranger* -- `Telegram - LMGTFY\_bot `__ by - *GabrielRF* -- `Telegram - UrlProBot `__ by - *GabrielRF* -- `Telegram Proxy - Bot `__ by - *Groosha* - A simple BITM (bot-in-the-middle) for Telegram acting as - some kind of "proxy". -- `Telegram Proxy Bot `__ by - *mrgigabyte* - - ``Credits for the original version of this bot goes to`` **Groosha** - ``, simply added certain features which I thought were needed``. -- `RadRetroRobot `__ by - *Tronikart* - Multifunctional Telegram Bot RadRetroRobot. -- `League of Legends bot `__ - (`source `__) by *i32ropie* -- `NeoBot `__ by *neoranger* -- `TagAlertBot `__ by *pitasi* - -Want to have your bot listed here? Send a Telegram message to @eternnoir -or @pevdh. - -.. |Download Month| image:: https://img.shields.io/pypi/v/pyTelegramBotAPI.svg - :target: https://pypi.python.org/pypi/pyTelegramBotAPI -.. |Build Status| image:: https://travis-ci.org/eternnoir/pyTelegramBotAPI.svg?branch=master - :target: https://travis-ci.org/eternnoir/pyTelegramBotAPI -.. |Download Month| image:: https://img.shields.io/pypi/dm/pyTelegramBotAPI.svg - :target: https://pypi.python.org/pypi/pyTelegramBotAPI diff --git a/examples/deep_linking.py b/examples/deep_linking.py index f92680f..f5ea506 100644 --- a/examples/deep_linking.py +++ b/examples/deep_linking.py @@ -1,3 +1,5 @@ +#!/usr/bin/python + # This example shows how to implement deep linking (https://core.telegram.org/bots#deep-linking) # with the pyTelegramBotAPI. # Note: This is not a working, production-ready sample. @@ -31,34 +33,38 @@ # steps are not shown here. Only steps 5 to 7 are illustrated, some in pseudo-code, with this example. import telebot -import time bot = telebot.TeleBot('TOKEN') + def extract_unique_code(text): # Extracts the unique_code from the sent /start command. return text.split()[1] if len(text.split()) > 1 else None + def in_storage(unique_code): # (pseudo-code) Should check if a unique code exists in storage return True + def get_username_from_storage(unique_code): # (pseudo-code) Does a query to the storage, retrieving the associated username # Should be replaced by a real database-lookup. return "ABC" if in_storage(unique_code) else None + def save_chat_id(chat_id, username): # (pseudo-code) Save the chat_id->username to storage # Should be replaced by a real database query. pass + @bot.message_handler(commands=['start']) def send_welcome(message): unique_code = extract_unique_code(message.text) - if unique_code: # if the '/start' command contains a unique_code + if unique_code: # if the '/start' command contains a unique_code username = get_username_from_storage(unique_code) - if username: # if the username exists in our database + if username: # if the username exists in our database save_chat_id(message.chat.id, username) reply = "Hello {0}, how are you?".format(username) else: @@ -67,4 +73,5 @@ def send_welcome(message): reply = "Please visit me via a provided URL from the website." bot.reply_to(message, reply) + bot.polling() diff --git a/examples/detailed_example/detailed_example.py b/examples/detailed_example/detailed_example.py index 8fad9af..f481832 100644 --- a/examples/detailed_example/detailed_example.py +++ b/examples/detailed_example/detailed_example.py @@ -2,9 +2,10 @@ This is a detailed example using almost every command of the API """ +import time + import telebot from telebot import types -import time TOKEN = '' @@ -12,10 +13,10 @@ knownUsers = [] # todo: save these in a file, userStep = {} # so they won't reset every time the bot restarts commands = { # command description used in the "help" command - 'start': 'Get used to the bot', - 'help': 'Gives you information about the available commands', - 'sendLongText': 'A test using the \'send_chat_action\' command', - 'getImage': 'A test using multi-stage messages, custom keyboard, and media sending' + 'start' : 'Get used to the bot', + 'help' : 'Gives you information about the available commands', + 'sendLongText': 'A test using the \'send_chat_action\' command', + 'getImage' : 'A test using multi-stage messages, custom keyboard, and media sending' } imageSelect = types.ReplyKeyboardMarkup(one_time_keyboard=True) # create the image selection keyboard @@ -33,7 +34,7 @@ def get_user_step(uid): else: knownUsers.append(uid) userStep[uid] = 0 - print "New user detected, who hasn't used \"/start\" yet" + print("New user detected, who hasn't used \"/start\" yet") return 0 @@ -45,7 +46,7 @@ def listener(messages): for m in messages: if m.content_type == 'text': # print the sent message to the console - print str(m.chat.first_name) + " [" + str(m.chat.id) + "]: " + m.text + print(str(m.chat.first_name) + " [" + str(m.chat.id) + "]: " + m.text) bot = telebot.TeleBot(TOKEN) @@ -128,4 +129,5 @@ def command_default(m): # this is the standard reply to a normal message bot.send_message(m.chat.id, "I don't understand \"" + m.text + "\"\nMaybe try the help page at /help") + bot.polling() diff --git a/examples/echo_bot.py b/examples/echo_bot.py index f88d3bf..b66eb34 100644 --- a/examples/echo_bot.py +++ b/examples/echo_bot.py @@ -1,3 +1,5 @@ +#!/usr/bin/python + # This is a simple echo bot using the decorator mechanism. # It echoes any incoming text messages. @@ -7,6 +9,7 @@ API_TOKEN = '' bot = telebot.TeleBot(API_TOKEN) + # Handle '/start' and '/help' @bot.message_handler(commands=['help', 'start']) def send_welcome(message): @@ -21,4 +24,5 @@ I am here to echo your kind words back to you. Just say anything nice and I'll s def echo_message(message): bot.reply_to(message, message.text) + bot.polling() diff --git a/examples/inline_example.py b/examples/inline_example.py index c97dea2..e36932d 100644 --- a/examples/inline_example.py +++ b/examples/inline_example.py @@ -1,8 +1,9 @@ -# This example show how to write an inline mode telegramt bot use pyTelegramBotAPI. -import telebot -import time -import sys +# This example show how to write an inline mode telegram bot use pyTelegramBotAPI. import logging +import sys +import time + +import telebot from telebot import types API_TOKEN = '' @@ -69,5 +70,5 @@ if __name__ == '__main__': try: main_loop() except KeyboardInterrupt: - print >> sys.stderr, '\nExiting by user request.\n' + print('\nExiting by user request.\n') sys.exit(0) diff --git a/examples/inline_keyboard_example.py b/examples/inline_keyboard_example.py new file mode 100644 index 0000000..0618eee --- /dev/null +++ b/examples/inline_keyboard_example.py @@ -0,0 +1,27 @@ +# This example show how to use inline keyboards and process button presses +import telebot +from telebot.types import InlineKeyboardMarkup, InlineKeyboardButton + +TELEGRAM_TOKEN = '' + +bot = telebot.TeleBot(TELEGRAM_TOKEN) + +def gen_markup(): + markup = InlineKeyboardMarkup() + markup.row_width = 2 + markup.add(InlineKeyboardButton("Yes", callback_data=f"cb_yes"), + InlineKeyboardButton("No", callback_data=f"cb_no")) + return markup + +@bot.callback_query_handler(func=lambda call: True) +def callback_query(call): + if call.data == "cb_yes": + bot.answer_callback_query(call.id, "Answer is Yes") + elif call.data == "cb_no": + bot.answer_callback_query(call.id, "Answer is No") + +@bot.message_handler(func=lambda message: True) +def message_handler(message): + bot.send_message(message.chat.id, "Yes/no?", reply_markup=gen_markup()) + +bot.polling(none_stop=True) diff --git a/examples/payments_example.py b/examples/payments_example.py index 3e82955..d0f52d4 100644 --- a/examples/payments_example.py +++ b/examples/payments_example.py @@ -1,6 +1,5 @@ import telebot -from telebot.types import LabeledPrice -from telebot.types import ShippingOption +from telebot.types import LabeledPrice, ShippingOption token = '1234567890:AAAABBBBCCCCDDDDeeeeFFFFgggGHHHH' provider_token = '1234567890:TEST:AAAABBBBCCCCDDDD' # @BotFather -> Bot Settings -> Payments diff --git a/examples/step_example.py b/examples/step_example.py index fd8d07d..0fc17e5 100644 --- a/examples/step_example.py +++ b/examples/step_example.py @@ -2,7 +2,6 @@ """ This Example will show you how to use register_next_step handler. """ -import time import telebot from telebot import types @@ -75,4 +74,13 @@ def process_sex_step(message): bot.reply_to(message, 'oooops') +# Enable saving next step handlers to file "./.handlers-saves/step.save". +# Delay=2 means that after any change in next step handlers (e.g. calling register_next_step_handler()) +# saving will hapen after delay 2 seconds. +bot.enable_save_next_step_handlers(delay=2) + +# Load next_step_handlers from save file (default "./.handlers-saves/step.save") +# WARNING It will work only if enable_save_next_step_handlers was called! +bot.load_next_step_handlers() + bot.polling() diff --git a/examples/telebot_bot/telebot_bot.py b/examples/telebot_bot/telebot_bot.py index cd29276..ac6b63c 100644 --- a/examples/telebot_bot/telebot_bot.py +++ b/examples/telebot_bot/telebot_bot.py @@ -3,9 +3,10 @@ # and goes by the name 'TeleBot (@pyTeleBot)'. Join our group to talk to him! # WARNING: Tested with Python 2.7 -import telebot import os +import telebot + text_messages = { 'welcome': u'Please welcome {name}!\n\n' @@ -33,8 +34,10 @@ if "TELEBOT_BOT_TOKEN" not in os.environ or "GROUP_CHAT_ID" not in os.environ: bot = telebot.AsyncTeleBot(os.environ["TELEBOT_BOT_TOKEN"]) GROUP_CHAT_ID = int(os.environ["GROUP_CHAT_ID"]) + def is_api_group(chat_id): - return chat_id== GROUP_CHAT_ID + return chat_id == GROUP_CHAT_ID + @bot.message_handler(func=lambda m: True, content_types=['new_chat_participant']) def on_user_joins(message): @@ -50,6 +53,7 @@ def on_user_joins(message): bot.reply_to(message, text_messages['welcome'].format(name=name)) + @bot.message_handler(commands=['info', 'help']) def on_info(message): if not is_api_group(message.chat.id): @@ -58,21 +62,23 @@ def on_info(message): bot.reply_to(message, text_messages['info']) + @bot.message_handler(commands=["ping"]) def on_ping(message): bot.reply_to(message, "Still alive and kicking!") + @bot.message_handler(commands=['start']) def on_start(message): if not is_api_group(message.chat.id): bot.reply_to(message, text_messages['wrong_chat']) return + def listener(messages): for m in messages: - print str(m) + print(str(m)) + bot.set_update_listener(listener) bot.polling() - - diff --git a/examples/webhook_examples/webhook_aiohttp_echo_bot.py b/examples/webhook_examples/webhook_aiohttp_echo_bot.py index d92cff9..bbb7c6e 100644 --- a/examples/webhook_examples/webhook_aiohttp_echo_bot.py +++ b/examples/webhook_examples/webhook_aiohttp_echo_bot.py @@ -11,7 +11,6 @@ from aiohttp import web import telebot - API_TOKEN = '' WEBHOOK_HOST = '' @@ -32,7 +31,6 @@ WEBHOOK_SSL_PRIV = './webhook_pkey.pem' # Path to the ssl private key WEBHOOK_URL_BASE = "https://{}:{}".format(WEBHOOK_HOST, WEBHOOK_PORT) WEBHOOK_URL_PATH = "/{}/".format(API_TOKEN) - logger = telebot.logger telebot.logger.setLevel(logging.INFO) @@ -51,6 +49,7 @@ async def handle(request): else: return web.Response(status=403) + app.router.add_post('/{token}/', handle) @@ -72,7 +71,7 @@ def echo_message(message): bot.remove_webhook() # Set webhook -bot.set_webhook(url=WEBHOOK_URL_BASE+WEBHOOK_URL_PATH, +bot.set_webhook(url=WEBHOOK_URL_BASE + WEBHOOK_URL_PATH, certificate=open(WEBHOOK_SSL_CERT, 'r')) # Build ssl context diff --git a/examples/webhook_examples/webhook_cherrypy_echo_bot.py b/examples/webhook_examples/webhook_cherrypy_echo_bot.py index d0f3da0..7b46e78 100644 --- a/examples/webhook_examples/webhook_cherrypy_echo_bot.py +++ b/examples/webhook_examples/webhook_cherrypy_echo_bot.py @@ -4,10 +4,11 @@ # This is a simple echo bot using decorators and webhook with CherryPy # It echoes any incoming text messages and does not use the polling method. -import cherrypy -import telebot import logging +import cherrypy + +import telebot API_TOKEN = '' @@ -29,7 +30,6 @@ WEBHOOK_SSL_PRIV = './webhook_pkey.pem' # Path to the ssl private key WEBHOOK_URL_BASE = "https://%s:%s" % (WEBHOOK_HOST, WEBHOOK_PORT) WEBHOOK_URL_PATH = "/%s/" % (API_TOKEN) - logger = telebot.logger telebot.logger.setLevel(logging.INFO) @@ -70,7 +70,7 @@ def echo_message(message): bot.remove_webhook() # Set webhook -bot.set_webhook(url=WEBHOOK_URL_BASE+WEBHOOK_URL_PATH, +bot.set_webhook(url=WEBHOOK_URL_BASE + WEBHOOK_URL_PATH, certificate=open(WEBHOOK_SSL_CERT, 'r')) # Disable CherryPy requests log @@ -80,9 +80,9 @@ for handler in tuple(access_log.handlers): # Start cherrypy server cherrypy.config.update({ - 'server.socket_host': WEBHOOK_LISTEN, - 'server.socket_port': WEBHOOK_PORT, - 'server.ssl_module': 'builtin', + 'server.socket_host' : WEBHOOK_LISTEN, + 'server.socket_port' : WEBHOOK_PORT, + 'server.ssl_module' : 'builtin', 'server.ssl_certificate': WEBHOOK_SSL_CERT, 'server.ssl_private_key': WEBHOOK_SSL_PRIV }) diff --git a/examples/webhook_examples/webhook_cpython_echo_bot.py b/examples/webhook_examples/webhook_cpython_echo_bot.py index 5c1dfc9..029f361 100644 --- a/examples/webhook_examples/webhook_cpython_echo_bot.py +++ b/examples/webhook_examples/webhook_cpython_echo_bot.py @@ -4,11 +4,17 @@ # This is a simple echo bot using decorators and webhook with BaseHTTPServer # It echoes any incoming text messages and does not use the polling method. -import BaseHTTPServer -import ssl -import telebot -import logging +try: + # Python 2 + from BaseHTTPServer import BaseHTTPRequestHandler, HTTPServer +except ImportError: + # Python 3 + from http.server import BaseHTTPRequestHandler, HTTPServer +import logging +import ssl + +import telebot API_TOKEN = '' @@ -30,7 +36,6 @@ WEBHOOK_SSL_PRIV = './webhook_pkey.pem' # Path to the ssl private key WEBHOOK_URL_BASE = "https://%s:%s" % (WEBHOOK_HOST, WEBHOOK_PORT) WEBHOOK_URL_PATH = "/%s/" % (API_TOKEN) - logger = telebot.logger telebot.logger.setLevel(logging.INFO) @@ -38,7 +43,7 @@ bot = telebot.TeleBot(API_TOKEN) # WebhookHandler, process webhook calls -class WebhookHandler(BaseHTTPServer.BaseHTTPRequestHandler): +class WebhookHandler(BaseHTTPRequestHandler): server_version = "WebhookHandler/1.0" def do_HEAD(self): @@ -84,12 +89,12 @@ def echo_message(message): bot.remove_webhook() # Set webhook -bot.set_webhook(url=WEBHOOK_URL_BASE+WEBHOOK_URL_PATH, +bot.set_webhook(url=WEBHOOK_URL_BASE + WEBHOOK_URL_PATH, certificate=open(WEBHOOK_SSL_CERT, 'r')) # Start server -httpd = BaseHTTPServer.HTTPServer((WEBHOOK_LISTEN, WEBHOOK_PORT), - WebhookHandler) +httpd = HTTPServer((WEBHOOK_LISTEN, WEBHOOK_PORT), + WebhookHandler) httpd.socket = ssl.wrap_socket(httpd.socket, certfile=WEBHOOK_SSL_CERT, diff --git a/examples/webhook_examples/webhook_flask_echo_bot.py b/examples/webhook_examples/webhook_flask_echo_bot.py index d0327d7..daa3995 100644 --- a/examples/webhook_examples/webhook_flask_echo_bot.py +++ b/examples/webhook_examples/webhook_flask_echo_bot.py @@ -4,11 +4,12 @@ # This is a simple echo bot using decorators and webhook with flask # It echoes any incoming text messages and does not use the polling method. -import flask -import telebot import logging import time +import flask + +import telebot API_TOKEN = '' @@ -30,7 +31,6 @@ WEBHOOK_SSL_PRIV = './webhook_pkey.pem' # Path to the ssl private key WEBHOOK_URL_BASE = "https://%s:%s" % (WEBHOOK_HOST, WEBHOOK_PORT) WEBHOOK_URL_PATH = "/%s/" % (API_TOKEN) - logger = telebot.logger telebot.logger.setLevel(logging.INFO) @@ -77,7 +77,7 @@ bot.remove_webhook() time.sleep(0.1) # Set webhook -bot.set_webhook(url=WEBHOOK_URL_BASE+WEBHOOK_URL_PATH, +bot.set_webhook(url=WEBHOOK_URL_BASE + WEBHOOK_URL_PATH, certificate=open(WEBHOOK_SSL_CERT, 'r')) # Start flask server diff --git a/examples/webhook_examples/webhook_flask_heroku_echo.py b/examples/webhook_examples/webhook_flask_heroku_echo.py index 62d0a90..7bbf2bf 100644 --- a/examples/webhook_examples/webhook_flask_heroku_echo.py +++ b/examples/webhook_examples/webhook_flask_heroku_echo.py @@ -1,8 +1,9 @@ import os -import telebot from flask import Flask, request +import telebot + TOKEN = '' bot = telebot.TeleBot(TOKEN) server = Flask(__name__) diff --git a/examples/webhook_examples/webhook_tornado_echo_bot.py b/examples/webhook_examples/webhook_tornado_echo_bot.py index 538b7b9..171d2d7 100644 --- a/examples/webhook_examples/webhook_tornado_echo_bot.py +++ b/examples/webhook_examples/webhook_tornado_echo_bot.py @@ -4,13 +4,15 @@ # This example shows webhook echo bot with Tornado web framework # Documenation to Tornado: http://tornadoweb.org -import telebot -import tornado.web -import tornado.ioloop -import tornado.httpserver -import tornado.options import signal +import tornado.httpserver +import tornado.ioloop +import tornado.options +import tornado.web + +import telebot + API_TOKEN = '' WEBHOOK_CERT = "./cert.pem" WEBHOOK_PKEY = "./pkey.pem" @@ -29,15 +31,18 @@ WEBHOOK_URL_BASE = "https://{0}:{1}/{2}".format(WEBHOOK_HOST, str(WEBHOOK_PORT), bot = telebot.TeleBot(API_TOKEN) + class Root(tornado.web.RequestHandler): def get(self): self.write("Hi! This is webhook example!") self.finish() + class webhook_serv(tornado.web.RequestHandler): def get(self): self.write("What are you doing here?") self.finish() + def post(self): if "Content-Length" in self.request.headers and \ "Content-Type" in self.request.headers and \ @@ -52,21 +57,26 @@ class webhook_serv(tornado.web.RequestHandler): else: self.write("What are you doing here?") self.finish() - + + tornado.options.define("port", default=WEBHOOK_PORT, help="run on the given port", type=int) is_closing = False + + def signal_handler(signum, frame): global is_closing print("Exiting...") is_closing = True + def try_exit(): global is_closing if is_closing: # clean up here tornado.ioloop.IOLoop.instance().stop() print("Exit success!") - + + # Handle '/start' and '/help' @bot.message_handler(commands=['help', 'start']) def send_welcome(message): @@ -74,6 +84,7 @@ def send_welcome(message): ("Hi there, I am EchoBot.\n" "I am here to echo your kind words back to you.")) + bot.remove_webhook() bot.set_webhook(url=WEBHOOK_URL_BASE, certificate=open(WEBHOOK_CERT, 'r')) @@ -86,9 +97,9 @@ application = tornado.web.Application([ ]) http_server = tornado.httpserver.HTTPServer(application, ssl_options={ - "certfile": WEBHOOK_CERT, - "keyfile": WEBHOOK_PKEY, - }) + "certfile": WEBHOOK_CERT, + "keyfile" : WEBHOOK_PKEY, +}) http_server.listen(tornado.options.options.port) tornado.ioloop.PeriodicCallback(try_exit, 100).start() tornado.ioloop.IOLoop.instance().start() diff --git a/setup.py b/setup.py index 1e76d03..a52df53 100644 --- a/setup.py +++ b/setup.py @@ -2,14 +2,15 @@ from setuptools import setup from io import open -def readme(): - with open('README.rst', encoding='utf-8') as f: - return f.read() +def read(filename): + with open(filename, encoding='utf-8') as file: + return file.read() setup(name='pyTelegramBotAPI', - version='3.6.3', + version='3.6.6', description='Python Telegram bot api. ', - long_description=readme(), + long_description=read('README.md'), + long_description_content_type="text/markdown", author='eternnoir', author_email='eternnoir@gmail.com', url='https://github.com/eternnoir/pyTelegramBotAPI', diff --git a/telebot/__init__.py b/telebot/__init__.py index 061d4d3..1d4053d 100644 --- a/telebot/__init__.py +++ b/telebot/__init__.py @@ -1,13 +1,15 @@ # -*- coding: utf-8 -*- from __future__ import print_function -import threading -import time +import logging +import os +import pickle import re import sys -import six +import threading +import time -import logging +import six logger = logging.getLogger('TeleBot') formatter = logging.Formatter( @@ -27,6 +29,72 @@ 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 Saver: + """ + Class for saving (next step|reply) handlers + """ + + def __init__(self, handlers, filename, delay): + self.handlers = handlers + self.filename = filename + self.delay = delay + self.timer = threading.Timer(delay, self.save_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, del_file_after_loading=True): + 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: + pickle.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: + handlers = pickle.load(file) + + if del_file_after_loading: + os.remove(filename) + + return handlers + + class TeleBot: """ This is TeleBot Class Methods: @@ -85,6 +153,9 @@ class TeleBot: # key: chat_id, value: handler list self.next_step_handlers = {} + self.next_step_saver = None + self.reply_saver = None + self.message_handlers = [] self.edited_message_handlers = [] self.channel_post_handlers = [] @@ -99,6 +170,54 @@ class TeleBot: if self.threaded: self.worker_pool = util.ThreadPool(num_threads=num_threads) + def enable_save_next_step_handlers(self, delay=120, filename="./.handler-saves/step.save"): + """ + Enable saving next step handlers (by default saving disable) + + :param delay: Delay between changes in handlers and saving + :param filename: Filename of save file + """ + self.next_step_saver = Saver(self.next_step_handlers, filename, delay) + + def enable_save_reply_handlers(self, delay=120, filename="./.handler-saves/reply.save"): + """ + Enable saving reply handlers (by default saving disable) + + :param delay: Delay between changes in handlers and saving + :param filename: Filename of save file + """ + self.reply_saver = Saver(self.reply_handlers, filename, delay) + + def disable_save_next_step_handlers(self): + """ + Disable saving next step handlers (by default saving disable) + """ + self.next_step_saver = None + + def disable_save_reply_handlers(self): + """ + Disable saving next step handlers (by default saving disable) + """ + self.reply_saver = None + + def load_next_step_handlers(self, filename="./.handler-saves/step.save", del_file_after_loading=True): + """ + Load next step handlers from save file + + :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_saver.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 + + :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_saver.load_handlers(filename) + def set_webhook(self, url=None, certificate=None, max_connections=None, allowed_updates=None): return apihelper.set_webhook(self.token, url, certificate, max_connections, allowed_updates) @@ -880,6 +999,12 @@ class TeleBot: return result return types.Message.de_json(result) + def edit_message_media(self, media, chat_id=None, message_id=None, inline_message_id=None, reply_markup=None): + 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=None, message_id=None, inline_message_id=None, reply_markup=None): result = apihelper.edit_message_reply_markup(self.token, chat_id, message_id, inline_message_id, reply_markup) if type(result) == bool: @@ -984,7 +1109,6 @@ class TeleBot: def get_sticker_set(self, name): """ Use this method to get a sticker set. On success, a StickerSet object is returned. - :param token: :param name: :return: """ @@ -1052,8 +1176,7 @@ class TeleBot: """ Registers a callback function to be notified when a reply to `message` arrives. - Warning: `message` must be sent with reply_markup=types.ForceReply(), otherwise TeleBot will not be able to see - the difference between a reply to `message` and an ordinary message. + 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` @@ -1066,17 +1189,18 @@ class TeleBot: """ Registers a callback function to be notified when a reply to `message` arrives. - Warning: `message` must be sent with reply_markup=types.ForceReply(), otherwise TeleBot will not be able to see - the difference between a reply to `message` and an ordinary message. + 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 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. """ if message_id in self.reply_handlers.keys(): - self.reply_handlers[message_id].append({"callback": callback, "args": args, "kwargs": kwargs}) + self.reply_handlers[message_id].append(Handler(callback, *args, **kwargs)) else: - self.reply_handlers[message_id] = [{"callback": callback, "args": args, "kwargs": kwargs}] + self.reply_handlers[message_id] = [Handler(callback, *args, **kwargs)] + if self.reply_saver is not None: + self.reply_saver.start_save_timer() def _notify_reply_handlers(self, new_messages): for message in new_messages: @@ -1087,11 +1211,15 @@ class TeleBot: for handler in handlers: self._exec_task(handler["callback"], message, *handler["args"], **handler["kwargs"]) self.reply_handlers.pop(reply_msg_id) + if self.reply_saver is not None: + self.reply_saver.start_save_timer() def register_next_step_handler(self, message, callback, *args, **kwargs): """ 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 @@ -1104,15 +1232,20 @@ class TeleBot: """ 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 """ if chat_id in self.next_step_handlers.keys(): - self.next_step_handlers[chat_id].append({"callback": callback, "args": args, "kwargs": kwargs}) + self.next_step_handlers[chat_id].append(Handler(callback, *args, **kwargs)) else: - self.next_step_handlers[chat_id] = [{"callback": callback, "args": args, "kwargs": kwargs}] + self.next_step_handlers[chat_id] = [Handler(callback, *args, **kwargs)] + + if self.next_step_saver is not None: + self.next_step_saver.start_save_timer() def clear_step_handler(self, message): """ @@ -1131,11 +1264,14 @@ class TeleBot: """ self.next_step_handlers[chat_id] = [] + if self.next_step_saver is not None: + self.next_step_saver.start_save_timer() + def clear_reply_handlers(self, message): """ Clears all callback functions registered by register_for_reply() and register_for_reply_by_message_id(). - :param message_id: The message for which we want to clear reply handlers + :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) @@ -1148,6 +1284,9 @@ class TeleBot: """ self.reply_handlers[message_id] = [] + if self.reply_saver is not None: + self.reply_saver.start_save_timer() + def _notify_next_handlers(self, new_messages): i = 0 while i < len(new_messages): @@ -1155,21 +1294,22 @@ class TeleBot: chat_id = message.chat.id was_poped = False if chat_id in self.next_step_handlers.keys(): - handlers = self.next_step_handlers[chat_id] - if (handlers): + handlers = self.next_step_handlers.pop(chat_id, None) + if handlers: for handler in handlers: self._exec_task(handler["callback"], message, *handler["args"], **handler["kwargs"]) new_messages.pop(i) # removing message that detects with next_step_handler was_poped = True - self.next_step_handlers.pop(chat_id, None) - if (not was_poped): + if self.next_step_saver is not None: + self.next_step_saver.start_save_timer() + if not was_poped: i += 1 @staticmethod def _build_handler_dict(handler, **filters): return { 'function': handler, - 'filters': filters + 'filters' : filters } def message_handler(self, commands=None, regexp=None, func=None, content_types=['text'], **kwargs): @@ -1332,7 +1472,8 @@ class TeleBot: return True - def _test_filter(self, filter, filter_value, message): + @staticmethod + def _test_filter(filter, filter_value, message): 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), @@ -1354,6 +1495,30 @@ class AsyncTeleBot(TeleBot): def __init__(self, *args, **kwargs): TeleBot.__init__(self, *args, **kwargs) + @util.async_dec() + def enable_save_next_step_handlers(self, delay=120, filename="./.handler-saves/step.save"): + return TeleBot.enable_save_next_step_handlers(self, delay, filename) + + @util.async_dec() + def enable_save_reply_handlers(self, delay=120, filename="./.handler-saves/reply.save"): + return TeleBot.enable_save_reply_handlers(self, delay, filename) + + @util.async_dec() + def disable_save_next_step_handlers(self): + return TeleBot.disable_save_next_step_handlers(self) + + @util.async_dec() + def disable_save_reply_handlers(self): + return TeleBot.enable_save_reply_handlers(self) + + @util.async_dec() + def load_next_step_handlers(self, filename="./.handler-saves/step.save", del_file_after_loading=True): + return TeleBot.load_next_step_handlers(self, filename, del_file_after_loading) + + @util.async_dec() + def load_reply_handlers(self, filename="./.handler-saves/reply.save", del_file_after_loading=True): + return TeleBot.load_reply_handlers(self, filename, del_file_after_loading) + @util.async_dec() def get_me(self): return TeleBot.get_me(self) @@ -1514,6 +1679,10 @@ class AsyncTeleBot(TeleBot): def edit_message_text(self, *args, **kwargs): return TeleBot.edit_message_text(self, *args, **kwargs) + @util.async_dec() + def edit_message_media(self, *args, **kwargs): + return TeleBot.edit_message_media(self, *args, **kwargs) + @util.async_dec() def edit_message_reply_markup(self, *args, **kwargs): return TeleBot.edit_message_reply_markup(self, *args, **kwargs) diff --git a/telebot/apihelper.py b/telebot/apihelper.py index e919b5a..8c04530 100644 --- a/telebot/apihelper.py +++ b/telebot/apihelper.py @@ -27,8 +27,8 @@ CONNECT_TIMEOUT = 3.5 READ_TIMEOUT = 9999 -def _get_req_session(): - return util.per_thread('req_session', lambda: requests.session()) +def _get_req_session(reset=False): + return util.per_thread('req_session', lambda: requests.session(), reset) def _make_request(token, method_name, method='get', params=None, files=None, base_url=API_URL): @@ -99,7 +99,6 @@ def get_file(token, file_id): def get_file_url(token, file_id): - method_url = r'getFile' return FILE_URL.format(token, get_file(token, file_id).file_path) @@ -123,6 +122,8 @@ def send_message(token, chat_id, text, disable_web_page_preview=None, reply_to_m :param disable_web_page_preview: :param reply_to_message_id: :param reply_markup: + :param parse_mode: + :param disable_notification: :return: """ method_url = r'sendMessage' @@ -266,7 +267,7 @@ def send_photo(token, chat_id, photo, caption=None, reply_to_message_id=None, re def send_media_group(token, chat_id, media, disable_notification=None, reply_to_message_id=None): method_url = r'sendMediaGroup' - media_json, files = _convert_input_media(media) + media_json, files = _convert_input_media_array(media) payload = {'chat_id': chat_id, 'media': media_json} if disable_notification: payload['disable_notification'] = disable_notification @@ -640,6 +641,21 @@ def edit_message_caption(token, caption, chat_id=None, message_id=None, inline_m return _make_request(token, method_url, params=payload) +def edit_message_media(token, media, chat_id=None, message_id=None, inline_message_id=None, reply_markup=None): + method_url = r'editMessageMedia' + media_json, file = _convert_input_media(media) + payload = {'media': media_json} + if chat_id: + payload['chat_id'] = chat_id + if message_id: + payload['message_id'] = message_id + if inline_message_id: + payload['inline_message_id'] = inline_message_id + if reply_markup: + payload['reply_markup'] = _convert_markup(reply_markup) + return _make_request(token, method_url, params=payload, files=file, method='post' if file else 'get') + + def edit_message_reply_markup(token, chat_id=None, message_id=None, inline_message_id=None, reply_markup=None): method_url = r'editMessageReplyMarkup' payload = {} @@ -756,7 +772,8 @@ def send_invoice(token, chat_id, title, description, invoice_payload, provider_t :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 - :return: + :param provider_data: + :return: """ method_url = r'sendInvoice' payload = {'chat_id': chat_id, 'title': title, 'description': description, 'payload': invoice_payload, @@ -939,11 +956,17 @@ def _convert_markup(markup): return markup -def _convert_input_media(array): +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.JsonSerializable): + if isinstance(input_media, types.InputMedia): media_dict = input_media.to_dic() if media_dict['media'].startswith('attach://'): key = media_dict['media'].replace('attach://', '') diff --git a/telebot/types.py b/telebot/types.py index 5ca1b4e..6165c94 100644 --- a/telebot/types.py +++ b/telebot/types.py @@ -10,7 +10,7 @@ import six from telebot import util -class JsonSerializable: +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. @@ -26,7 +26,7 @@ class JsonSerializable: raise NotImplementedError -class Dictionaryable: +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_dic. @@ -42,7 +42,7 @@ class Dictionaryable: raise NotImplementedError -class JsonDeserializable: +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. @@ -457,11 +457,11 @@ class Message(JsonDeserializable): if not entities: return text _subs = { - "bold": "{text}", - "italic": "{text}", - "pre": "

{text}
", - "code": "{text}", - "url": "{text}", + "bold" : "{text}", + "italic" : "{text}", + "pre" : "
{text}
", + "code" : "{text}", + "url" : "{text}", "text_link": "{text}" } if hasattr(self, "custom_subs"): @@ -469,6 +469,7 @@ class Message(JsonDeserializable): _subs[type] = self.custom_subs[type] utf16_text = text.encode("utf-16-le") html_text = "" + def func(text, type=None, url=None, user=None): text = text.decode("utf-16-le") if type == "text_mention": @@ -501,6 +502,7 @@ class Message(JsonDeserializable): def html_caption(self): return self.__html_text(self.caption, self.caption_entities) + class MessageEntity(JsonDeserializable): @classmethod def de_json(cls, json_string): @@ -598,29 +600,6 @@ class Document(JsonDeserializable): self.file_size = file_size -class Sticker(JsonDeserializable): - @classmethod - def de_json(cls, json_string): - obj = cls.check_json(json_string) - file_id = obj['file_id'] - width = obj['width'] - height = obj['height'] - thumb = None - if 'thumb' in obj: - thumb = PhotoSize.de_json(obj['thumb']) - emoji = obj.get('emoji') - file_size = obj.get('file_size') - return cls(file_id, width, height, thumb, emoji, file_size) - - def __init__(self, file_id, width, height, thumb, emoji=None, file_size=None): - self.file_id = file_id - self.width = width - self.height = height - self.thumb = thumb - self.emoji = emoji - self.file_size = file_size - - class Video(JsonDeserializable): @classmethod def de_json(cls, json_string): @@ -1092,7 +1071,7 @@ class InputVenueMessageContent(Dictionaryable): def to_dic(self): json_dic = {'latitude': self.latitude, 'longitude': self.longitude, 'title': self.title, - 'address': self.address} + 'address' : self.address} if self.foursquare_id: json_dic['foursquare_id'] = self.foursquare_id return json_dic @@ -1191,7 +1170,7 @@ class InlineQueryResultArticle(JsonSerializable): class InlineQueryResultPhoto(JsonSerializable): def __init__(self, id, photo_url, thumb_url, photo_width=None, photo_height=None, title=None, - description=None, caption=None, reply_markup=None, input_message_content=None): + description=None, caption=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 @@ -1202,6 +1181,8 @@ class InlineQueryResultPhoto(JsonSerializable): :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: @@ -1215,6 +1196,7 @@ class InlineQueryResultPhoto(JsonSerializable): self.title = title self.description = description self.caption = caption + self.parse_mode = parse_mode self.reply_markup = reply_markup self.input_message_content = input_message_content @@ -1230,6 +1212,8 @@ class InlineQueryResultPhoto(JsonSerializable): json_dict['description'] = self.description if self.caption: json_dict['caption'] = self.caption + if self.parse_mode: + json_dict['parse_mode'] = self.parse_mode if self.reply_markup: json_dict['reply_markup'] = self.reply_markup.to_dic() if self.input_message_content: @@ -1286,7 +1270,7 @@ class InlineQueryResultGif(JsonSerializable): class InlineQueryResultMpeg4Gif(JsonSerializable): def __init__(self, id, mpeg4_url, thumb_url, mpeg4_width=None, mpeg4_height=None, title=None, caption=None, - reply_markup=None, input_message_content=None, mpeg4_duration=None): + parse_mode=None, reply_markup=None, input_message_content=None, mpeg4_duration=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 @@ -1296,6 +1280,8 @@ class InlineQueryResultMpeg4Gif(JsonSerializable): :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: @@ -1308,6 +1294,7 @@ class InlineQueryResultMpeg4Gif(JsonSerializable): self.thumb_url = thumb_url self.title = title self.caption = caption + self.parse_mode = parse_mode self.reply_markup = reply_markup self.input_message_content = input_message_content self.mpeg4_duration = mpeg4_duration @@ -1322,6 +1309,8 @@ class InlineQueryResultMpeg4Gif(JsonSerializable): json_dict['title'] = self.title if self.caption: json_dict['caption'] = self.caption + if self.parse_mode: + json_dict['parse_mode'] = self.parse_mode if self.reply_markup: json_dict['reply_markup'] = self.reply_markup.to_dic() if self.input_message_content: @@ -1333,8 +1322,8 @@ class InlineQueryResultMpeg4Gif(JsonSerializable): class InlineQueryResultVideo(JsonSerializable): def __init__(self, id, video_url, mime_type, thumb_url, title, - caption=None, video_width=None, video_height=None, video_duration=None, description=None, - reply_markup=None, input_message_content=None): + caption=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 @@ -1342,6 +1331,8 @@ class InlineQueryResultVideo(JsonSerializable): :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 @@ -1358,6 +1349,7 @@ class InlineQueryResultVideo(JsonSerializable): self.thumb_url = thumb_url self.title = title self.caption = caption + self.parse_mode = parse_mode self.description = description self.input_message_content = input_message_content self.reply_markup = reply_markup @@ -1375,6 +1367,8 @@ class InlineQueryResultVideo(JsonSerializable): json_dict['description'] = self.description if self.caption: json_dict['caption'] = self.caption + if self.parse_mode: + json_dict['parse_mode'] = self.parse_mode if self.reply_markup: json_dict['reply_markup'] = self.reply_markup.to_dic() if self.input_message_content: @@ -1383,13 +1377,14 @@ class InlineQueryResultVideo(JsonSerializable): class InlineQueryResultAudio(JsonSerializable): - def __init__(self, id, audio_url, title, caption=None, performer=None, audio_duration=None, reply_markup=None, - input_message_content=None): + def __init__(self, id, audio_url, title, caption=None, parse_mode=None, performer=None, audio_duration=None, + reply_markup=None, input_message_content=None): self.type = 'audio' self.id = id self.audio_url = audio_url self.title = title self.caption = caption + self.parse_mode = parse_mode self.performer = performer self.audio_duration = audio_duration self.reply_markup = reply_markup @@ -1399,6 +1394,8 @@ class InlineQueryResultAudio(JsonSerializable): json_dict = {'type': self.type, 'id': self.id, 'audio_url': self.audio_url, 'title': self.title} if self.caption: json_dict['caption'] = self.caption + if self.parse_mode: + json_dict['parse_mode'] = self.parse_mode if self.performer: json_dict['performer'] = self.performer if self.audio_duration: @@ -1411,13 +1408,14 @@ class InlineQueryResultAudio(JsonSerializable): class InlineQueryResultVoice(JsonSerializable): - def __init__(self, id, voice_url, title, caption=None, performer=None, voice_duration=None, reply_markup=None, - input_message_content=None): + def __init__(self, id, voice_url, title, caption=None, parse_mode=None, performer=None, voice_duration=None, + reply_markup=None, input_message_content=None): self.type = 'voice' self.id = id self.voice_url = voice_url self.title = title self.caption = caption + self.parse_mode = parse_mode self.performer = performer self.voice_duration = voice_duration self.reply_markup = reply_markup @@ -1427,6 +1425,8 @@ class InlineQueryResultVoice(JsonSerializable): json_dict = {'type': self.type, 'id': self.id, 'voice_url': self.voice_url, 'title': self.title} if self.caption: json_dict['caption'] = self.caption + if self.parse_mode: + json_dict['parse_mode'] = self.parse_mode if self.performer: json_dict['performer'] = self.performer if self.voice_duration: @@ -1439,14 +1439,15 @@ class InlineQueryResultVoice(JsonSerializable): class InlineQueryResultDocument(JsonSerializable): - def __init__(self, id, title, document_url, mime_type, caption=None, description=None, reply_markup=None, - input_message_content=None, thumb_url=None, thumb_width=None, thumb_height=None): + def __init__(self, id, title, document_url, mime_type, caption=None, parse_mode=None, description=None, + reply_markup=None, input_message_content=None, thumb_url=None, thumb_width=None, thumb_height=None): self.type = 'document' self.id = id self.title = title self.document_url = document_url self.mime_type = mime_type self.caption = caption + self.parse_mode = parse_mode self.description = description self.reply_markup = reply_markup self.input_message_content = input_message_content @@ -1459,6 +1460,8 @@ class InlineQueryResultDocument(JsonSerializable): 'mime_type': self.mime_type} if self.caption: json_dict['caption'] = self.caption + if self.parse_mode: + json_dict['parse_mode'] = self.parse_mode if self.description: json_dict['description'] = self.description if self.thumb_url: @@ -1581,6 +1584,7 @@ class BaseInlineQueryResultCached(JsonSerializable): self.caption = None self.reply_markup = None self.input_message_content = None + self.parse_mode = None self.payload_dic = {} def to_json(self): @@ -1597,12 +1601,14 @@ class BaseInlineQueryResultCached(JsonSerializable): json_dict['reply_markup'] = self.reply_markup.to_dic() if self.input_message_content: json_dict['input_message_content'] = self.input_message_content.to_dic() + if self.parse_mode: + json_dict['parse_mode'] = self.parse_mode return json.dumps(json_dict) class InlineQueryResultCachedPhoto(BaseInlineQueryResultCached): - def __init__(self, id, photo_file_id, title=None, description=None, caption=None, reply_markup=None, - input_message_content=None): + def __init__(self, id, photo_file_id, title=None, description=None, caption=None, parse_mode=None, + reply_markup=None, input_message_content=None): BaseInlineQueryResultCached.__init__(self) self.type = 'photo' self.id = id @@ -1612,11 +1618,12 @@ class InlineQueryResultCachedPhoto(BaseInlineQueryResultCached): self.caption = caption 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(BaseInlineQueryResultCached): - def __init__(self, id, gif_file_id, title=None, description=None, caption=None, reply_markup=None, + def __init__(self, id, gif_file_id, title=None, description=None, caption=None, parse_mode=None, reply_markup=None, input_message_content=None): BaseInlineQueryResultCached.__init__(self) self.type = 'gif' @@ -1627,12 +1634,13 @@ class InlineQueryResultCachedGif(BaseInlineQueryResultCached): self.caption = caption 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(BaseInlineQueryResultCached): - def __init__(self, id, mpeg4_file_id, title=None, description=None, caption=None, reply_markup=None, - input_message_content=None): + def __init__(self, id, mpeg4_file_id, title=None, description=None, caption=None, parse_mode=None, + reply_markup=None, input_message_content=None): BaseInlineQueryResultCached.__init__(self) self.type = 'mpeg4_gif' self.id = id @@ -1642,6 +1650,7 @@ class InlineQueryResultCachedMpeg4Gif(BaseInlineQueryResultCached): self.caption = caption 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 @@ -1657,7 +1666,7 @@ class InlineQueryResultCachedSticker(BaseInlineQueryResultCached): class InlineQueryResultCachedDocument(BaseInlineQueryResultCached): - def __init__(self, id, document_file_id, title, description=None, caption=None, reply_markup=None, + def __init__(self, id, document_file_id, title, description=None, caption=None, parse_mode=None, reply_markup=None, input_message_content=None): BaseInlineQueryResultCached.__init__(self) self.type = 'document' @@ -1668,11 +1677,12 @@ class InlineQueryResultCachedDocument(BaseInlineQueryResultCached): self.caption = caption 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(BaseInlineQueryResultCached): - def __init__(self, id, video_file_id, title, description=None, caption=None, reply_markup=None, + def __init__(self, id, video_file_id, title, description=None, caption=None, parse_mode=None, reply_markup=None, input_message_content=None): BaseInlineQueryResultCached.__init__(self) self.type = 'video' @@ -1683,11 +1693,13 @@ class InlineQueryResultCachedVideo(BaseInlineQueryResultCached): self.caption = caption 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(BaseInlineQueryResultCached): - def __init__(self, id, voice_file_id, title, caption=None, reply_markup=None, input_message_content=None): + def __init__(self, id, voice_file_id, title, caption=None, parse_mode=None, reply_markup=None, + input_message_content=None): BaseInlineQueryResultCached.__init__(self) self.type = 'voice' self.id = id @@ -1696,11 +1708,12 @@ class InlineQueryResultCachedVoice(BaseInlineQueryResultCached): self.caption = caption 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(BaseInlineQueryResultCached): - def __init__(self, id, audio_file_id, caption=None, reply_markup=None, input_message_content=None): + def __init__(self, id, audio_file_id, caption=None, parse_mode=None, reply_markup=None, input_message_content=None): BaseInlineQueryResultCached.__init__(self) self.type = 'audio' self.id = id @@ -1708,6 +1721,7 @@ class InlineQueryResultCachedAudio(BaseInlineQueryResultCached): self.caption = caption 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 @@ -2041,48 +2055,61 @@ class MaskPosition(JsonDeserializable, JsonSerializable): # InputMedia -class InputMediaPhoto(JsonSerializable): - def __init__(self, media, caption=None, parse_mode=None): - self.type = "photo" +class InputMedia(JsonSerializable): + def __init__(self, type, media, caption=None, parse_mode=None): + self.type = type self.media = media self.caption = caption self.parse_mode = parse_mode + 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_dic()) def to_dic(self): - ret = {'type': self.type, 'media': 'attach://' + util.generate_random_token() - if not util.is_string(self.media) else self.media} + ret = {'type': self.type, 'media': self._media_dic} if self.caption: ret['caption'] = self.caption if self.parse_mode: ret['parse_mode'] = self.parse_mode return ret + def _convert_input_media(self): + if util.is_string(self.media): + return self.to_json(), None -class InputMediaVideo(JsonSerializable): - def __init__(self, media, caption=None, parse_mode=None, width=None, height=None, duration=None, + return self.to_json(), {self._media_name: self.media} + + +class InputMediaPhoto(InputMedia): + def __init__(self, media, caption=None, parse_mode=None): + super(InputMediaPhoto, self).__init__(type="photo", media=media, caption=caption, parse_mode=parse_mode) + + def to_dic(self): + ret = super(InputMediaPhoto, self).to_dic() + return ret + + +class InputMediaVideo(InputMedia): + def __init__(self, media, thumb=None, caption=None, parse_mode=None, width=None, height=None, duration=None, supports_streaming=None): - self.type = "video" - self.media = media - self.caption = caption - self.parse_mode = parse_mode + 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_json(self): - return json.dumps(self.to_dic()) - def to_dic(self): - ret = {'type': self.type, 'media': 'attach://' + util.generate_random_token() - if not util.is_string(self.media) else self.media} - if self.caption: - ret['caption'] = self.caption - if self.parse_mode: - ret['parse_mode'] = self.parse_mode + ret = super(InputMediaVideo, self).to_dic() + if self.thumb: + ret['thumb'] = self.thumb if self.width: ret['width'] = self.width if self.height: @@ -2092,3 +2119,57 @@ class InputMediaVideo(JsonSerializable): 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_dic(self): + ret = super(InputMediaAnimation, self).to_dic() + 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_dic(self): + ret = super(InputMediaAudio, self).to_dic() + 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): + super(InputMediaDocument, self).__init__(type="document", media=media, caption=caption, parse_mode=parse_mode) + self.thumb = thumb + + def to_dic(self): + ret = super(InputMediaDocument, self).to_dic() + if self.thumb: + ret['thumb'] = self.thumb + return ret diff --git a/telebot/util.py b/telebot/util.py index f448d78..21826d0 100644 --- a/telebot/util.py +++ b/telebot/util.py @@ -1,10 +1,11 @@ # -*- coding: utf-8 -*- import random +import re import string +import sys import threading import traceback -import re -import sys + import six from six import string_types @@ -243,18 +244,17 @@ def extract_arguments(text): :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("\/\w*(@\w*)*\s*([\s\S]*)",re.IGNORECASE) + regexp = re.compile("/\w*(@\w*)*\s*([\s\S]*)",re.IGNORECASE) result = regexp.match(text) return result.group(2) if is_command(text) else None -def per_thread(key, construct_value): - try: - return getattr(thread_local, key) - except AttributeError: +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 value + + return getattr(thread_local, key) def generate_random_token(): diff --git a/tests/test_telebot.py b/tests/test_telebot.py index 6818b5b..908b141 100644 --- a/tests/test_telebot.py +++ b/tests/test_telebot.py @@ -361,6 +361,20 @@ class TestTeleBot: new_msg = tb.edit_message_caption(caption='Edit test', chat_id=CHAT_ID, message_id=msg.message_id) assert new_msg.caption == 'Edit test' + def test_edit_message_media(self): + file_data = open('../examples/detailed_example/kitten.jpg', 'rb') + file_data_2 = open('../examples/detailed_example/rooster.jpg', 'rb') + tb = telebot.TeleBot(TOKEN) + msg = tb.send_photo(CHAT_ID, file_data) + new_msg = tb.edit_message_media(chat_id=CHAT_ID, message_id=msg.message_id, + media=types.InputMediaPhoto(file_data_2, caption='Test editMessageMedia 0')) + assert type(new_msg) != bool + + new_msg = tb.edit_message_media(chat_id=CHAT_ID, message_id=msg.message_id, + media=types.InputMediaPhoto(msg.photo[0].file_id, caption='Test editMessageMedia')) + assert type(new_msg) != bool + assert new_msg.caption == 'Test editMessageMedia' + def test_get_chat(self): tb = telebot.TeleBot(TOKEN) ch = tb.get_chat(GROUP_ID) @@ -388,7 +402,8 @@ class TestTeleBot: new_msg = tb.edit_message_reply_markup(chat_id=CHAT_ID, message_id=ret_msg.message_id, reply_markup=markup) assert new_msg.message_id - def create_text_message(self, text): + @staticmethod + def create_text_message(text): params = {'text': text} chat = types.User(11, False, 'test') return types.Message(1, None, None, chat, 'text', params, "") diff --git a/tests/test_types.py b/tests/test_types.py index c174d42..c229bdf 100644 --- a/tests/test_types.py +++ b/tests/test_types.py @@ -59,7 +59,7 @@ def test_json_Message_Sticker_without_thumb(): json_string = r'{"message_id":98,"from":{"id":10734,"first_name":"Fd","last_name":"Wd","username":"dd","is_bot":true },"chat":{"id":10734,"first_name":"Fd","type":"private","last_name":"Wd","username":"dd"},"date":1435479551,"sticker":{"width":550,"height":368,"file_id":"BQADBQADNAIAAsYifgYdGJOa6bGAsQI","file_size":30320}}' msg = types.Message.de_json(json_string) assert msg.sticker.height == 368 - assert msg.sticker.thumb == None + assert msg.sticker.thumb is None assert msg.content_type == 'sticker' @@ -113,6 +113,7 @@ def test_json_voice(): assert voice.duration == 0 assert voice.file_size == 10481 + def test_json_update(): json_string = r'{"update_id":938203,"message":{"message_id":241,"from":{"is_bot":true,"id":9734,"first_name":"Fk","last_name":"Wg","username":"nir"},"chat":{"id":1111,"first_name":"Fk","type":"private","last_name":"Wg","username":"oir"},"date":1441447009,"text":"HIHI"}}' update = types.Update.de_json(json_string) @@ -120,6 +121,7 @@ def test_json_update(): assert update.message.message_id == 241 assert update.message.from_user.id == 9734 + def test_json_chat(): json_string = r'{"id": -111111,"title": "Test Title","type": "group"}' chat = types.Chat.de_json(json_string) @@ -127,6 +129,7 @@ def test_json_chat(): assert chat.type == 'group' assert chat.title == 'Test Title' + def test_InlineQueryResultCachedPhoto(): iq = types.InlineQueryResultCachedPhoto('aaa', 'Fileid') json_str = iq.to_json() @@ -143,6 +146,7 @@ def test_InlineQueryResultCachedPhoto_with_title(): assert 'Title' in json_str assert 'caption' not in json_str + def test_InlineQueryResultCachedPhoto_with_markup(): markup = types.InlineKeyboardMarkup() markup.add(types.InlineKeyboardButton("Google", url="http://www.google.com")) @@ -154,4 +158,3 @@ def test_InlineQueryResultCachedPhoto_with_markup(): assert 'Title' in json_str assert 'caption' not in json_str assert 'reply_markup' in json_str -