diff --git a/README.md b/README.md index 7953711..fc6e8b0 100644 --- a/README.md +++ b/README.md @@ -663,5 +663,6 @@ Get help. Discuss. Chat. * [RandomInfoBot](https://t.me/RandomInfoBot) by [@Akash](https://github.com/BLUE-DEVIL1134) - A Telegram Bot Which Generates Random Information Of Humans Scraped From Over 13 Websites. * [TasksListsBot](https://t.me/TasksListsBot) ([source](https://github.com/Pablo-Davila/TasksListsBot)) by [@Pablo-Davila](https://github.com/Pablo-Davila) - A (tasks) lists manager bot for Telegram. * [MyElizaPsychologistBot](https://t.me/TasksListsBot) ([source](https://github.com/Pablo-Davila/MyElizaPsychologistBot)) by [@Pablo-Davila](https://github.com/Pablo-Davila) - An implementation of the famous Eliza psychologist chatbot. +* [Evdembot](https://t.me/Evdembot) by Adem Kavak. A bot that informs you about everything you want. Want to have your bot listed here? Send a Telegram message to @eternnoir or @pevdh. diff --git a/telebot/__init__.py b/telebot/__init__.py index 1c74d02..df59f39 100644 --- a/telebot/__init__.py +++ b/telebot/__init__.py @@ -93,7 +93,7 @@ class TeleBot: def __init__( self, token, parse_mode=None, threaded=True, skip_pending=False, num_threads=2, - next_step_backend=None, reply_backend=None, exception_handler=None + next_step_backend=None, reply_backend=None, exception_handler=None, last_update_id=0 ): """ :param token: bot API token @@ -107,7 +107,7 @@ class TeleBot: self.skip_pending = skip_pending self.__stop_polling = threading.Event() - self.last_update_id = 0 + self.last_update_id = last_update_id self.exc_info = None self.next_step_backend = next_step_backend diff --git a/telebot/types.py b/telebot/types.py index 2d004fa..3519533 100644 --- a/telebot/types.py +++ b/telebot/types.py @@ -385,6 +385,8 @@ class Message(JsonDeserializable): if 'passport_data' in obj: opts['passport_data'] = obj['passport_data'] content_type = 'passport_data' + if 'reply_markup' in obj: + opts['reply_markup'] = InlineKeyboardMarkup.de_json(obj['reply_markup']) return cls(message_id, from_user, date, chat, content_type, opts, json_string) @classmethod @@ -455,6 +457,7 @@ class Message(JsonDeserializable): self.invoice = None self.successful_payment = None self.connected_website = None + self.reply_markup = None for key in options: setattr(self, key, options[key]) self.json = json_string @@ -586,13 +589,15 @@ class PhotoSize(JsonDeserializable): if (json_string is None): return None obj = cls.check_json(json_string) file_id = obj['file_id'] + file_unique_id = obj['file_unique_id'] width = obj['width'] height = obj['height'] file_size = obj.get('file_size') - return cls(file_id, width, height, file_size) + return cls(file_id, file_unique_id, width, height, file_size) - def __init__(self, file_id, width, height, file_size=None): + def __init__(self, file_id, file_unique_id, width, height, file_size=None): self.file_size = file_size + self.file_unique_id = file_unique_id self.height = height self.width = width self.file_id = file_id @@ -604,15 +609,17 @@ class Audio(JsonDeserializable): if (json_string is None): return None obj = cls.check_json(json_string) file_id = obj['file_id'] + file_unique_id = obj['file_unique_id'] duration = obj['duration'] performer = obj.get('performer') title = obj.get('title') mime_type = obj.get('mime_type') file_size = obj.get('file_size') - return cls(file_id, duration, performer, title, mime_type, file_size) + return cls(file_id, file_unique_id, duration, performer, title, mime_type, file_size) - def __init__(self, file_id, duration, performer=None, title=None, mime_type=None, file_size=None): + def __init__(self, file_id, file_unique_id, duration, performer=None, title=None, mime_type=None, file_size=None): self.file_id = file_id + self.file_unique_id = file_unique_id self.duration = duration self.performer = performer self.title = title @@ -626,13 +633,15 @@ class Voice(JsonDeserializable): if (json_string is None): return None obj = cls.check_json(json_string) file_id = obj['file_id'] + file_unique_id = obj['file_unique_id'] duration = obj['duration'] mime_type = obj.get('mime_type') file_size = obj.get('file_size') - return cls(file_id, duration, mime_type, file_size) + return cls(file_id, file_unique_id, duration, mime_type, file_size) - def __init__(self, file_id, duration, mime_type=None, file_size=None): + def __init__(self, file_id, file_unique_id, duration, mime_type=None, file_size=None): self.file_id = file_id + self.file_unique_id = file_unique_id self.duration = duration self.mime_type = mime_type self.file_size = file_size @@ -644,16 +653,18 @@ class Document(JsonDeserializable): if (json_string is None): return None obj = cls.check_json(json_string) file_id = obj['file_id'] + file_unique_id = obj['file_unique_id'] thumb = None if 'thumb' in obj and 'file_id' in obj['thumb']: thumb = PhotoSize.de_json(obj['thumb']) file_name = obj.get('file_name') mime_type = obj.get('mime_type') file_size = obj.get('file_size') - return cls(file_id, thumb, file_name, mime_type, file_size) + return cls(file_id, file_unique_id, thumb, file_name, mime_type, file_size) - def __init__(self, file_id, thumb=None, file_name=None, mime_type=None, file_size=None): + def __init__(self, file_id, file_unique_id, thumb=None, file_name=None, mime_type=None, file_size=None): self.file_id = file_id + self.file_unique_id = file_unique_id self.thumb = thumb self.file_name = file_name self.mime_type = mime_type @@ -667,16 +678,18 @@ class Video(JsonDeserializable): return None obj = cls.check_json(json_string) file_id = obj['file_id'] + file_unique_id = obj['file_unique_id'] width = obj['width'] height = obj['height'] duration = obj['duration'] thumb = PhotoSize.de_json(obj.get('thumb')) mime_type = obj.get('mime_type') file_size = obj.get('file_size') - return cls(file_id, width, height, duration, thumb, mime_type, file_size) + return cls(file_id, file_unique_id, width, height, duration, thumb, mime_type, file_size) - def __init__(self, file_id, width, height, duration, thumb=None, mime_type=None, file_size=None): + def __init__(self, file_id, file_unique_id, width, height, duration, thumb=None, mime_type=None, file_size=None): self.file_id = file_id + self.file_unique_id = file_unique_id self.width = width self.height = height self.duration = duration @@ -692,14 +705,16 @@ class VideoNote(JsonDeserializable): return None obj = cls.check_json(json_string) file_id = obj['file_id'] + file_unique_id = obj['file_unique_id'] length = obj['length'] duration = obj['duration'] thumb = PhotoSize.de_json(obj.get('thumb')) file_size = obj.get('file_size') - return cls(file_id, length, duration, thumb, file_size) + return cls(file_id, file_unique_id, length, duration, thumb, file_size) - def __init__(self, file_id, length, duration, thumb=None, file_size=None): + def __init__(self, file_id, file_unique_id, length, duration, thumb=None, file_size=None): self.file_id = file_id + self.file_unique_id = file_unique_id self.length = length self.duration = duration self.thumb = thumb @@ -785,12 +800,14 @@ class File(JsonDeserializable): return None obj = cls.check_json(json_string) file_id = obj['file_id'] + file_unique_id = obj['file_unique_id'] file_size = obj.get('file_size') file_path = obj.get('file_path') - return cls(file_id, file_size, file_path) + return cls(file_id, file_unique_id, file_size, file_path) - def __init__(self, file_id, file_size, file_path): + def __init__(self, file_id, file_unique_id, file_size, file_path): self.file_id = file_id + self.file_unique_id = file_unique_id self.file_size = file_size self.file_path = file_path @@ -923,10 +940,18 @@ class KeyboardButtonPollType(Dictionaryable): return {'type': self.type} -class InlineKeyboardMarkup(Dictionaryable, JsonSerializable): +class InlineKeyboardMarkup(Dictionaryable, JsonSerializable, JsonDeserializable): max_row_keys = 8 + + @classmethod + def de_json(cls, json_string): + if (json_string is None): + return None + obj = cls.check_json(json_string) + keyboard = [[InlineKeyboardButton.de_json(button) for button in row] for row in obj['inline_keyboard']] + return cls(keyboard) - def __init__(self, row_width=3): + def __init__(self, keyboard=[] ,row_width=3): """ This object represents an inline keyboard that appears right next to the message it belongs to. @@ -939,7 +964,7 @@ class InlineKeyboardMarkup(Dictionaryable, JsonSerializable): row_width = self.max_row_keys self.row_width = row_width - self.keyboard = [] + self.keyboard = keyboard def add(self, *args, row_width=None): """ @@ -965,7 +990,7 @@ class InlineKeyboardMarkup(Dictionaryable, JsonSerializable): row_width = self.max_row_keys for row in util.chunks(args, row_width): - button_array = [button.to_dict() for button in row] + button_array = [button for button in row] self.keyboard.append(button_array) return self @@ -995,16 +1020,28 @@ class InlineKeyboardMarkup(Dictionaryable, JsonSerializable): return json.dumps(self.to_dict()) def to_dict(self): - json_dict = {'inline_keyboard': self.keyboard} + json_dict = dict() + json_dict['inline_keyboard'] = [[button.to_dict() for button in row] for row in self.keyboard] return json_dict -class LoginUrl(Dictionaryable, JsonSerializable): +class LoginUrl(Dictionaryable, JsonSerializable, JsonDeserializable): def __init__(self, url, forward_text=None, bot_username=None, request_write_access=None): self.url = url self.forward_text = forward_text self.bot_username = bot_username self.request_write_access = request_write_access + + @classmethod + def de_json(cls, json_string): + if (json_string is None): + return None + obj = cls.check_json(json_string) + url = obj['url'] + forward_text = obj.get('forward_text') + bot_username = obj.get('bot_username') + request_write_access = obj.get('request_write_access') + return cls(url, forward_text, bot_username, request_write_access) def to_json(self): return json.dumps(self.to_dict()) @@ -1020,7 +1057,7 @@ class LoginUrl(Dictionaryable, JsonSerializable): return json_dict -class InlineKeyboardButton(Dictionaryable, JsonSerializable): +class InlineKeyboardButton(Dictionaryable, JsonSerializable, JsonDeserializable): def __init__(self, text, url=None, callback_data=None, switch_inline_query=None, switch_inline_query_current_chat=None, callback_game=None, pay=None, login_url=None): self.text = text @@ -1031,6 +1068,21 @@ class InlineKeyboardButton(Dictionaryable, JsonSerializable): self.callback_game = callback_game self.pay = pay self.login_url = login_url + + @classmethod + def de_json(cls, json_string): + if (json_string is None): + return None + obj = cls.check_json(json_string) + text = obj['text'] + url = obj.get('url') + callback_data = obj.get('callback_data') + switch_inline_query = obj.get('switch_inline_query') + switch_inline_query_current_chat = obj.get('switch_inline_query_current_chat') + callback_game = obj.get('callback_game') + pay = obj.get('pay') + login_url = LoginUrl.de_json(obj.get('login_url')) + return cls(text, url, callback_data, switch_inline_query, switch_inline_query_current_chat, callback_game, pay, login_url) def to_json(self): return json.dumps(self.to_dict()) @@ -1085,12 +1137,16 @@ class ChatPhoto(JsonDeserializable): return None obj = cls.check_json(json_string) small_file_id = obj['small_file_id'] + small_file_unique_id = obj['small_file_unique_id'] big_file_id = obj['big_file_id'] - return cls(small_file_id, big_file_id) + big_file_unique_id = obj['big_file_unique_id'] + return cls(small_file_id, small_file_unique_id, big_file_id, big_file_unique_id) - def __init__(self, small_file_id, big_file_id): + def __init__(self, small_file_id, small_file_unique_id, big_file_id, big_file_unique_id): self.small_file_id = small_file_id + self.small_file_unique_id = small_file_unique_id self.big_file_id = big_file_id + self.big_file_unique_id = big_file_unique_id class ChatMember(JsonDeserializable): @@ -2034,14 +2090,16 @@ class Animation(JsonDeserializable): if (json_string is None): return None obj = cls.check_json(json_string) file_id = obj['file_id'] + file_unique_id = obj['file_unique_id'] thumb = PhotoSize.de_json(obj.get('thumb')) file_name = obj.get('file_name') mime_type = obj.get('mime_type') file_size = obj.get('file_size') - return cls(file_id, thumb, file_name, mime_type, file_size) + return cls(file_id, file_unique_id, thumb, file_name, mime_type, file_size) - def __init__(self, file_id, thumb=None, file_name=None, mime_type=None, file_size=None): + def __init__(self, file_id, file_unique_id, thumb=None, file_name=None, mime_type=None, file_size=None): self.file_id = file_id + self.file_unique_id = file_unique_id self.thumb = thumb self.file_name = file_name self.mime_type = mime_type diff --git a/telebot/util.py b/telebot/util.py index 099828a..438e8c1 100644 --- a/telebot/util.py +++ b/telebot/util.py @@ -11,7 +11,7 @@ import queue as Queue import logging try: - import PIL + from PIL import Image from io import BytesIO pil_imported = True except: @@ -164,7 +164,7 @@ def is_bytes(var): return isinstance(var, bytes) def is_pil_image(var): - return pil_imported and isinstance(var, PIL.Image.Image) + return pil_imported and isinstance(var, Image.Image) def pil_image_to_file(image, extension='JPEG', quality='web_low'): if pil_imported: diff --git a/tests/test_types.py b/tests/test_types.py index d8c1403..173cda9 100644 --- a/tests/test_types.py +++ b/tests/test_types.py @@ -17,6 +17,28 @@ def test_json_message(): assert msg.text == 'HIHI' +def test_json_message_with_reply_markup(): + jsonstring = r'{"message_id":48,"from":{"id":153587469,"is_bot":false,"first_name":"Neko","username":"Neko"},"chat":{"id":153587469,"first_name":"Neko","username":"Neko","type":"private"},"date":1598879570,"text":"test","reply_markup":{"inline_keyboard":[[{"text":"Google","url":"http://www.google.com"},{"text":"Yahoo","url":"http://www.yahoo.com"}]]}}' + msg = types.Message.de_json(jsonstring) + assert msg.content_type == 'text' + assert msg.reply_markup.keyboard[0][0].text == 'Google' + + +def test_json_InlineKeyboardMarkup(): + jsonstring = r'{"inline_keyboard":[[{"text":"Google","url":"http://www.google.com"},{"text":"Yahoo","url":"http://www.yahoo.com"}]]}' + markup = types.InlineKeyboardMarkup.de_json(jsonstring) + assert markup.keyboard[0][0].text == 'Google' + assert markup.keyboard[0][1].url == 'http://www.yahoo.com' + + +def test_json_InlineKeyboardButton(): + jsonstring = r'{"text":"Google","url":"http://www.google.com"}' + button = types.InlineKeyboardButton.de_json(jsonstring) + assert button.text == 'Google' + assert button.url == 'http://www.google.com' + + + def test_json_message_with_dice(): jsonstring = r'{"message_id":5560,"from":{"id":879343317,"is_bot":false,"first_name":"George","last_name":"Forse","username":"dr_fxrse","language_code":"ru"},"chat":{"id":879343317,"first_name":"George","last_name":"Forse","username":"dr_fxrse","type":"private"},"date":1586926330,"dice":{"value": 4, "emoji": "\ud83c\udfaf"}}' msg = types.Message.de_json(jsonstring) @@ -41,14 +63,14 @@ def test_json_GroupChat(): def test_json_Document(): - json_string = r'{"file_name":"Text File","thumb":{},"file_id":"BQADBQADMwIAAsYifgZ_CEh0u682xwI","file_size":446}' + json_string = r'{"file_name":"Text File","thumb":{},"file_id":"BQADBQADMwIAAsYifgZ_CEh0u682xwI","file_unique_id": "AgADJQEAAqfhOEY","file_size":446}' doc = types.Document.de_json(json_string) assert doc.thumb is None assert doc.file_name == 'Text File' def test_json_Message_Audio(): - json_string = r'{"message_id":131,"from":{"id":12775,"first_name":"dd","username":"dd","is_bot":true },"chat":{"id":10834,"first_name":"dd","type":"private","type":"private","last_name":"dd","username":"dd"},"date":1439978364,"audio":{"duration":1,"mime_type":"audio\/mpeg","title":"pyTelegram","performer":"eternnoir","file_id":"BQADBQADDH1JaB8-1KyWUss2-Ag","file_size":20096}}' + json_string = r'{"message_id":131,"from":{"id":12775,"first_name":"dd","username":"dd","is_bot":true },"chat":{"id":10834,"first_name":"dd","type":"private","type":"private","last_name":"dd","username":"dd"},"date":1439978364,"audio":{"duration":1,"mime_type":"audio\/mpeg","title":"pyTelegram","performer":"eternnoir","file_id":"BQADBQADDH1JaB8-1KyWUss2-Ag","file_unique_id": "AgADawEAAn8VSFY","file_size":20096}}' msg = types.Message.de_json(json_string) assert msg.audio.duration == 1 assert msg.content_type == 'audio' @@ -73,21 +95,21 @@ def test_json_Message_Sticker_without_thumb(): def test_json_Message_Document(): - json_string = r'{"message_id":97,"from":{"id":10734,"first_name":"Fd","last_name":"Wd","username":"dd","is_bot":true },"chat":{"id":10,"first_name":"Fd","type":"private","last_name":"Wd","username":"dd"},"date":1435478744,"document":{"file_name":"Text File","thumb":{},"file_id":"BQADBQADMwIAAsYifgZ_CEh0u682xwI","file_size":446}}' + json_string = r'{"message_id":97,"from":{"id":10734,"first_name":"Fd","last_name":"Wd","username":"dd","is_bot":true },"chat":{"id":10,"first_name":"Fd","type":"private","last_name":"Wd","username":"dd"},"date":1435478744,"document":{"file_name":"Text File","thumb":{},"file_id":"BQADBQADMwIAAsYifgZ_CEh0u682xwI","file_unique_id": "AQAD_QIfa3QAAyA4BgAB","file_size":446}}' msg = types.Message.de_json(json_string) assert msg.document.file_name == 'Text File' assert msg.content_type == 'document' def test_json_Message_Photo(): - json_string = r'{"message_id":96,"from":{"id":109734,"first_name":"Fd","last_name":"Wd","username":"dd","is_bot":true },"chat":{"id":10734,"first_name":"Fd","type":"private","last_name":"dd","username":"dd"},"date":1435478191,"photo":[{"file_id":"AgADBQADIagxG8YifgYv8yLSj76i-dd","file_size":615,"width":90,"height":67},{"file_id":"AgADBQADIagxG8YifgYv8yLSj76i-dd","file_size":10174,"width":320,"height":240},{"file_id":"dd-A_LsTIABFNx-FUOaEa_3AABAQABAg","file_size":53013,"width":759,"height":570}]}' + json_string = r'{"message_id":96,"from":{"id":109734,"first_name":"Fd","last_name":"Wd","username":"dd","is_bot":true },"chat":{"id":10734,"first_name":"Fd","type":"private","last_name":"dd","username":"dd"},"date":1435478191,"photo":[{"file_id":"AgADBQADIagxG8YifgYv8yLSj76i-dd","file_unique_id": "AQAD_QIfa3QAAyA4BgAB","file_size":615,"width":90,"height":67},{"file_id":"AgADBQADIagxG8YifgYv8yLSj76i-dd","file_unique_id": "AQAD_QIfa3QAAyA4BgAB","file_size":10174,"width":320,"height":240},{"file_id":"dd-A_LsTIABFNx-FUOaEa_3AABAQABAg","file_unique_id": "AQAD_QIfa3QAAyA4BgAB","file_size":53013,"width":759,"height":570}]}' msg = types.Message.de_json(json_string) assert len(msg.photo) == 3 assert msg.content_type == 'photo' def test_json_Message_Video(): - json_string = r'{"message_id":101,"from":{"id":109734,"first_name":"dd","last_name":"dd","username":"dd","is_bot":true },"chat":{"id":109734,"first_name":"dd","type":"private","last_name":"dd","username":"dd"},"date":1435481960,"video":{"duration":3,"caption":"","width":360,"height":640,"thumb":{"file_id":"AAQFABPiYnBjkDwMAAIC","file_size":1597,"width":50,"height":90},"file_id":"BAADBQADNifgb_TOPEKErGoQI","file_size":260699}}' + json_string = r'{"message_id":101,"from":{"id":109734,"first_name":"dd","last_name":"dd","username":"dd","is_bot":true },"chat":{"id":109734,"first_name":"dd","type":"private","last_name":"dd","username":"dd"},"date":1435481960,"video":{"duration":3,"caption":"","width":360,"height":640,"thumb":{"file_id":"AAQFABPiYnBjkDwMAAIC","file_unique_id": "AQADTeisa3QAAz1nAAI","file_size":1597,"width":50,"height":90},"file_id":"BAADBQADNifgb_TOPEKErGoQI","file_unique_id": "AgADbgEAAn8VSFY","file_size":260699}}' msg = types.Message.de_json(json_string) assert msg.video assert msg.video.duration == 3 @@ -103,7 +125,7 @@ def test_json_Message_Location(): def test_json_UserProfilePhotos(): - json_string = r'{"total_count":1,"photos":[[{"file_id":"AgADAgADqacxG6wpRwABvEB6fpeIcKS4HAIkAATZH_SpyZjzIwdVAAIC","file_size":6150,"width":160,"height":160},{"file_id":"AgADAgADqacxG6wpRwABvEB6fpeIcKS4HAIkAATOiTNi_YoJMghVAAIC","file_size":13363,"width":320,"height":320},{"file_id":"AgADAgADqacxG6wpRwABvEB6fpeIcKS4HAIkAAQW4DyFv0-lhglVAAIC","file_size":28347,"width":640,"height":640},{"file_id":"AgADAgADqacxG6wpRwABvEB6fpeIcKS4HAIkAAT50RvJCg0GQApVAAIC","file_size":33953,"width":800,"height":800}]]}' + json_string = r'{"total_count":1,"photos":[[{"file_id":"AgADAgADqacxG6wpRwABvEB6fpeIcKS4HAIkAATZH_SpyZjzIwdVAAIC","file_unique_id": "AQAD_QIfa3QAAyA4BgAB","file_size":6150,"width":160,"height":160},{"file_id":"AgADAgADqacxG6wpRwABvEB6fpeIcKS4HAIkAATOiTNi_YoJMghVAAIC","file_unique_id": "AQAD_QIfa3QAAyA4BgAB","file_size":13363,"width":320,"height":320},{"file_id":"AgADAgADqacxG6wpRwABvEB6fpeIcKS4HAIkAAQW4DyFv0-lhglVAAIC","file_unique_id": "AQAD_QIfa3QAAyA4BgAB","file_size":28347,"width":640,"height":640},{"file_id":"AgADAgADqacxG6wpRwABvEB6fpeIcKS4HAIkAAT50RvJCg0GQApVAAIC","file_unique_id": "AQAD_QIfa3QAAyA4BgAB","file_size":33953,"width":800,"height":800}]]}' upp = types.UserProfilePhotos.de_json(json_string) assert upp.photos[0][0].width == 160 assert upp.photos[0][-1].height == 800 @@ -117,7 +139,7 @@ def test_json_contact(): def test_json_voice(): - json_string = r'{"duration": 0,"mime_type": "audio/ogg","file_id": "AwcccccccDH1JaB7w_gyFjYQxVAg","file_size": 10481}' + json_string = r'{"duration": 0,"mime_type": "audio/ogg","file_id": "AwcccccccDH1JaB7w_gyFjYQxVAg","file_unique_id": "AgADbAEAAn8VSFY","file_size": 10481}' voice = types.Voice.de_json(json_string) assert voice.duration == 0 assert voice.file_size == 10481