diff --git a/README.md b/README.md index 043fa9a..4166581 100644 --- a/README.md +++ b/README.md @@ -1,12 +1,13 @@ # pyTelegramBotAPI -Python Telegram Bot API. +A Python implementation for the Telegram Bot API. -[https://core.telegram.org/bots/api](https://core.telegram.org/bots/api) +See [https://core.telegram.org/bots/api](https://core.telegram.org/bots/api) ## How to install -* Need python2 or python3 +Python 2 or Python 3 is required. + * Install from source ``` @@ -15,7 +16,7 @@ $ cd pyTelegramBotAPI $ python setup.py install ``` -* or install by pip +* or install with pip ``` $ pip install pyTelegramBotAPI @@ -23,7 +24,7 @@ $ pip install pyTelegramBotAPI ## Example -* Send Message +* Sending a message. ```python import telebot @@ -31,8 +32,8 @@ import telebot TOKEN = '' tb = telebot.TeleBot(TOKEN) -# tb.send_message(chatid,message) -print tb.send_message(281281, 'gogo power ranger') +# tb.send_message(chatid, message) +tb.send_message(281281, 'gogo power ranger') ``` * Echo Bot @@ -45,23 +46,21 @@ TOKEN = '' def listener(*messages): """ - When new message get will call this function. - :param messages: - :return: + When new messages arrive TeleBot will call this function. """ for m in messages: chatid = m.chat.id - if m.content_type == 'text' + if m.content_type == 'text': text = m.text tb.send_message(chatid, text) tb = telebot.TeleBot(TOKEN) -tb.get_update() # cache exist message tb.set_update_listener(listener) #register listener -tb.polling(3) -while True: - time.sleep(20) +tb.polling() + +while True: # Don't let the main Thread end. + pass ``` ## TeleBot API usage @@ -71,7 +70,7 @@ import telebot import time TOKEN = '' -tb = telebot.TeleBot(TOKEN) #create new Telegram Bot object +tb = telebot.TeleBot(TOKEN) #create a new Telegram Bot object # getMe user = tb.get_me() @@ -107,43 +106,59 @@ tb.send_video(chat_id, video) tb.send_location(chat_id, lat, lon) # sendChatAction -# action_string can be : typing,upload_photo,record_video,upload_video,record_audio,upload_audio,upload_document, -# find_location. +# 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) -# ReplyKeyboardMarkup. -# Use ReplyKeyboardMarkup class. +# Use the ReplyKeyboardMarkup class. # Thanks pevdh. from telebot import types markup = types.ReplyKeyboardMarkup() markup.add('a', 'v', 'd') -tb.send_message(chat_id, message, None, None, markup) -# or use row method +tb.send_message(chat_id, message, reply_markup=markup) + +# or add strings one row at a time: markup = types.ReplyKeyboardMarkup() markup.row('a', 'v') markup.row('c', 'd', 'e') -tb.send_message(chat_id, message, None, None, markup) +tb.send_message(chat_id, message, reply_markup=markup) ``` -## Message notifier - -* Define listener function - +## Creating a Telegram bot with the pyTelegramBotAPI +There are two ways to define a Telegram Bot with the pyTelegramBotAPI. +### The listener mechanism +* First, create a TeleBot instance. ```python -def listener1(*messages): +import telebot + +TOKEN = '' + +bot = telebot.TeleBot(TOKEN) +``` +* Then, define a listener function. +```python +def echo_messages(*messages): + """ + Echoes all incoming messages of content_type 'text'. + """ for m in messages: chatid = m.chat.id - if m.content_type == 'text' + if m.content_type == 'text': text = m.text - tb.send_message(chatid, text) + bot.send_message(chatid, text) ``` +* Now, register your listener with the TeleBot instance and call TeleBot#polling() +```python +bot.set_update_listener(echo_messages) +bot.polling() -* Use ***set_update_listener*** method to add listener function to telebot. -* Start polling or call get_update(). If get new updates, telebot will call listener and pass messages to listener. -* use Message's content_type attribute to check message type. Now Message support content_type: +while True: # Don't let the main Thread end. + pass +``` +* use Message's content_type attribute to check the type of Message. Now Message supports content types: * text * audio * document @@ -151,6 +166,49 @@ def listener1(*messages): * sticker * video * location +* That's it! + +### The decorator mechanism +* First, create a TeleBot instance. +```python +import telebot + +TOKEN = '' + +bot = telebot.TeleBot(TOKEN) +``` +* Next, define all of your so-called message handlers and decorate them with @bot.message_handler +```python +# Handle /start and /help +@bot.message_handler(commands=['start', 'help']) +def command_help(message): + bot.reply_to(message, "Hello, did someone call for help?") + +# Handles all messages which text matches the regex regexp. +# See https://en.wikipedia.org/wiki/Regular_expression +# This regex matches all sent url's. +@bot.message_handler(regexp='((https?):((//)|(\\\\))+([\w\d:#@%/;$()~_?\+-=\\\.&](#!)?)*)') +def command_url(message): + bot.reply_to(message, "I shouldn't open that url, should I?") + +# 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.reply_to(message, "Document received, sir!") + +# Default command handler. A lambda expression which always returns True is used for this purpose. +@bot.message_handler(func=lambda message: True, content_types=['audio', 'video', 'document', 'text', 'location', 'contact', 'sticker']) +def default_command(message): + bot.reply_to(message, "This is the default command handler.") +``` +* And finally, call bot.polling() +```python +bot.polling() + +while True: # Don't end the main thread. + pass +``` +Use whichever mechanism fits your purpose! It is even possible to mix and match. ## TODO @@ -165,4 +223,4 @@ def listener1(*messages): - [x] sendLocation - [x] sendChatAction - [x] getUserProfilePhotos -- [ ] getUpdat(chat message not yet) +- [ ] getUpdate(chat message not yet) diff --git a/examples/echo_bot.py b/examples/echo_bot.py new file mode 100644 index 0000000..d3538a8 --- /dev/null +++ b/examples/echo_bot.py @@ -0,0 +1,28 @@ +# This is a simple echo bot using the decorator mechanism. +# It echoes any incoming text messages. + +import telebot + +API_TOKEN = '' + +bot = telebot.TeleBot(API_TOKEN) + + +# Handle '/start' and '/help' +@bot.message_handler(commands=['help, start']) +def send_welcome(message): + bot.reply_to(message, """\ +Hi there, I am EchoBot. +I am here to echo your kind words back to you. Just say anything nice and I'll say the exact same thing to you!\ +""") + + +# Handle all other messages with content_type 'text' (content_types defaults to ['text']) +@bot.message_handler(func=lambda message: True) +def echo_message(message): + bot.reply_to(message, message.text) + +bot.polling() + +while True: + pass diff --git a/telebot/__init__.py b/telebot/__init__.py index 39ba280..fb1287d 100644 --- a/telebot/__init__.py +++ b/telebot/__init__.py @@ -41,7 +41,7 @@ class TeleBot: self.last_update_id = 0 - self.commands = [] + self.message_handlers = [] def get_update(self): """ @@ -49,7 +49,7 @@ class TeleBot: Registered listeners and applicable message handlers will be notified when a new message arrives. :raises ApiException when a call has failed. """ - updates = apihelper.get_updates(self.token, offset=(self.last_update_id + 1)) + updates = apihelper.get_updates(self.token, offset=(self.last_update_id + 1), timeout=20) new_messages = [] for update in updates: if update['update_id'] > self.last_update_id: @@ -66,31 +66,32 @@ class TeleBot: t = threading.Thread(target=listener, args=new_messages) t.start() - def polling(self, interval=3): + def polling(self): """ + This function creates a new Thread that calls an internal __polling function. + This allows the bot to retrieve Updates automagically and notify listeners and message handlers accordingly. + + Do not call this function more than once! + Always get updates. - :param interval: interval secs. :return: """ - self.interval = interval - # clear thread. - self.__stop_polling = True - time.sleep(interval + 1) self.__stop_polling = False self.polling_thread = threading.Thread(target=self.__polling, args=()) self.polling_thread.daemon = True self.polling_thread.start() def __polling(self): - print('telegram bot start polling') + print('TeleBot: Started polling.') while not self.__stop_polling: try: self.get_update() except Exception as e: + print("TeleBot: Exception occurred. Stopping.") + self.__stop_polling = True print(e) - time.sleep(self.interval) - print('telegram bot stop polling') + print('TeleBot: Stopped polling.') def stop_polling(self): self.__stop_polling = True @@ -109,7 +110,7 @@ class TeleBot: :param user_id: :param offset: :param limit: - :return: + :return: API reply. """ result = apihelper.get_user_profile_photos(self.token, user_id, offset, limit) return types.UserProfilePhotos.de_json(result) @@ -122,10 +123,11 @@ class TeleBot: :param disable_web_page_preview: :param reply_to_message_id: :param reply_markup: - :return: + :return: API reply. """ - return apihelper.send_message(self.token, chat_id, text, disable_web_page_preview, reply_to_message_id, - reply_markup) + return types.Message.de_json( + apihelper.send_message(self.token, chat_id, text, disable_web_page_preview, reply_to_message_id, + reply_markup)) def forward_message(self, chat_id, from_chat_id, message_id): """ @@ -133,9 +135,9 @@ class TeleBot: :param chat_id: which chat to forward :param from_chat_id: which chat message from :param message_id: message id - :return: + :return: API reply. """ - return apihelper.forward_message(self.token, chat_id, from_chat_id, message_id) + return types.Message.de_json(apihelper.forward_message(self.token, chat_id, from_chat_id, message_id)) def send_photo(self, chat_id, photo, caption=None, reply_to_message_id=None, reply_markup=None): """ @@ -145,9 +147,10 @@ class TeleBot: :param caption: :param reply_to_message_id: :param reply_markup: - :return: + :return: API reply. """ - return apihelper.send_photo(self.token, chat_id, photo, caption, reply_to_message_id, reply_markup) + return types.Message.de_json( + apihelper.send_photo(self.token, chat_id, photo, caption, reply_to_message_id, reply_markup)) def send_audio(self, chat_id, data, reply_to_message_id=None, reply_markup=None): """ @@ -157,9 +160,10 @@ class TeleBot: :param data: :param reply_to_message_id: :param reply_markup: - :return: + :return: API reply. """ - return apihelper.send_data(self.token, chat_id, data, 'audio', reply_to_message_id, reply_markup) + return types.Message.de_json( + apihelper.send_data(self.token, chat_id, data, 'audio', reply_to_message_id, reply_markup)) def send_document(self, chat_id, data, reply_to_message_id=None, reply_markup=None): """ @@ -168,9 +172,10 @@ class TeleBot: :param data: :param reply_to_message_id: :param reply_markup: - :return: + :return: API reply. """ - return apihelper.send_data(self.token, chat_id, data, 'document', reply_to_message_id, reply_markup) + return types.Message.de_json( + apihelper.send_data(self.token, chat_id, data, 'document', reply_to_message_id, reply_markup)) def send_sticker(self, chat_id, data, reply_to_message_id=None, reply_markup=None): """ @@ -179,9 +184,10 @@ class TeleBot: :param data: :param reply_to_message_id: :param reply_markup: - :return: + :return: API reply. """ - return apihelper.send_data(self.token, chat_id, data, 'sticker', reply_to_message_id, reply_markup) + return types.Message.de_json( + apihelper.send_data(self.token, chat_id, data, 'sticker', reply_to_message_id, reply_markup)) def send_video(self, chat_id, data, reply_to_message_id=None, reply_markup=None): """ @@ -190,9 +196,10 @@ class TeleBot: :param data: :param reply_to_message_id: :param reply_markup: - :return: + :return: API reply. """ - return apihelper.send_data(self.token, chat_id, data, 'video', reply_to_message_id, reply_markup) + return types.Message.de_json( + apihelper.send_data(self.token, chat_id, data, 'video', reply_to_message_id, reply_markup)) def send_location(self, chat_id, latitude, longitude, reply_to_message_id=None, reply_markup=None): """ @@ -202,9 +209,10 @@ class TeleBot: :param longitude: :param reply_to_message_id: :param reply_markup: - :return: + :return: API reply. """ - return apihelper.send_location(self.token, chat_id, latitude, longitude, reply_to_message_id, reply_markup) + return types.Message.de_json( + apihelper.send_location(self.token, chat_id, latitude, longitude, reply_to_message_id, reply_markup)) def send_chat_action(self, chat_id, action): """ @@ -212,13 +220,16 @@ class TeleBot: The status is set for 5 seconds or less (when a message arrives from your bot, Telegram clients clear its typing status). :param chat_id: - :param action: string . typing,upload_photo,record_video,upload_video,record_audio,upload_audio,upload_document, - find_location. - :return: + :param action: One of the following strings: 'typing', 'upload_photo', 'record_video', 'upload_video', + 'record_audio', 'upload_audio', 'upload_document', 'find_location'. + :return: API reply. """ - return apihelper.send_chat_action(self.token, chat_id, action) + return types.Message.de_json(apihelper.send_chat_action(self.token, chat_id, action)) - def message_handler(self, regexp=None, func=None, content_types=['text']): + def reply_to(self, message, text, **kwargs): + return self.send_message(message.chat.id, text, reply_to_message_id=message.message_id, **kwargs) + + def message_handler(self, commands=None, regexp=None, func=None, content_types=['text']): """ Message handler decorator. This decorator can be used to decorate functions that must handle certain types of messages. @@ -246,27 +257,148 @@ class TeleBot: :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: This commands' supported content types. Must be a list. Defaults to ['text']. - :return: """ + def decorator(fn): - self.commands.append([fn, regexp, func, content_types]) + func_dict = {'function': fn, 'content_types': content_types} + if regexp: + func_dict['regexp'] = regexp if 'text' in content_types else None + if func: + func_dict['lambda'] = func + if commands: + func_dict['commands'] = commands if 'text' in content_types else None + self.message_handlers.append(func_dict) return fn + return decorator @staticmethod - def _test_command(command, message): - if message.content_type not in command[3]: + def is_command(text): + """ + 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. + """ + return text.startswith('/') + + @staticmethod + def extract_command(text): + """ + 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('/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, else None. + """ + return text.split()[0][1:] if TeleBot.is_command(text) else None + + @staticmethod + def _test_message_handler(message_handler, message): + if message.content_type not in message_handler['content_types']: return False - if command[1] is not None and message.content_type == 'text' and re.search(command[1], message.text): - return True - if command[2] is not None: - return command[2](message) + if 'commands' in message_handler and message.content_type == 'text': + return TeleBot.extract_command(message.text) in message_handler['commands'] + if 'regexp' in message_handler and message.content_type == 'text' and re.search(message_handler['regexp'], + message.text): + return False + if 'lambda' in message_handler: + return message_handler['lambda'](message) return False def _notify_command_handlers(self, new_messages): for message in new_messages: - for command in self.commands: - if self._test_command(command, message): - t = threading.Thread(target=command[0], args=(message,)) + for message_handler in self.message_handlers: + if self._test_message_handler(message_handler, message): + t = threading.Thread(target=message_handler['function'], args=(message,)) t.start() break + + +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, Exception): + raise self.result + else: + return self.result + + +def async(): + def decorator(fn): + def wrapper(*args, **kwargs): + return AsyncTask(fn, *args, **kwargs) + + return wrapper + + return decorator + + +class AsyncTeleBot(TeleBot): + def __init__(self, *args, **kwargs): + TeleBot.__init__(self, *args, **kwargs) + + @async() + def get_me(self): + return TeleBot.get_me(self) + + @async() + def get_user_profile_photos(self, *args, **kwargs): + return TeleBot.get_user_profile_photos(self, *args, **kwargs) + + @async() + def send_message(self, *args, **kwargs): + return TeleBot.send_message(self, *args, **kwargs) + + @async() + def forward_message(self, *args, **kwargs): + return TeleBot.forward_message(self, *args, **kwargs) + + @async() + def send_photo(self, *args, **kwargs): + return TeleBot.send_photo(self, *args, **kwargs) + + @async() + def send_audio(self, *args, **kwargs): + return TeleBot.send_audio(self, *args, **kwargs) + + @async() + def send_document(self, *args, **kwargs): + return TeleBot.send_document(self, *args, **kwargs) + + @async() + def send_sticker(self, *args, **kwargs): + return TeleBot.send_sticker(self, *args, **kwargs) + + @async() + def send_video(self, *args, **kwargs): + return TeleBot.send_video(self, *args, **kwargs) + + @async() + def send_location(self, *args, **kwargs): + return TeleBot.send_location(self, *args, **kwargs) + + @async() + def send_chat_action(self, *args, **kwargs): + return TeleBot.send_chat_action(self, *args, **kwargs) diff --git a/telebot/apihelper.py b/telebot/apihelper.py index 8fee253..0715f2b 100644 --- a/telebot/apihelper.py +++ b/telebot/apihelper.py @@ -56,12 +56,16 @@ def send_message(token, chat_id, text, disable_web_page_preview=None, reply_to_m return _make_request(token, method_url, params=payload) -def get_updates(token, offset=None): +def get_updates(token, offset=None, limit=None, timeout=None): method_url = r'getUpdates' - if offset is not None: - return _make_request(token, method_url, params={'offset': offset}) - else: - return _make_request(token, method_url) + payload = {} + if offset: + payload['offset'] = offset + if limit: + payload['limit'] = limit + if timeout: + payload['timeout'] = timeout + return _make_request(token, method_url, params=payload) def get_user_profile_photos(token, user_id, offset=None, limit=None): diff --git a/telebot/types.py b/telebot/types.py index c3c9d90..7056a77 100644 --- a/telebot/types.py +++ b/telebot/types.py @@ -290,6 +290,7 @@ class Contact(JsonDeserializable): phone_number = obj['phone_number'] first_name = obj['first_name'] last_name = None + user_id = None if 'last_name' in obj: last_name = obj['last_name'] if 'user_id' in obj: