From 3e33b7f1cb7990e28104ac2a0454a72f7adb439d Mon Sep 17 00:00:00 2001 From: MAIKS1900 Date: Sat, 26 Jun 2021 14:36:14 +0300 Subject: [PATCH 1/2] Bot API 5.3 changes - Personalized Commands for different chats - Custom Placeholders of input field for ReplyKeyboardMarkup and ForceReply. --- telebot/__init__.py | 43 ++++++++++++++++++++++++---- telebot/apihelper.py | 31 +++++++++++++++++---- telebot/types.py | 65 +++++++++++++++++++++++++++++++++++++++++-- tests/test_telebot.py | 18 ++++++++++++ 4 files changed, 142 insertions(+), 15 deletions(-) diff --git a/telebot/__init__.py b/telebot/__init__.py index f06ee7323..753e7d3fd 100644 --- a/telebot/__init__.py +++ b/telebot/__init__.py @@ -1703,21 +1703,52 @@ def delete_chat_photo(self, chat_id: Union[int, str]) -> bool: """ return apihelper.delete_chat_photo(self.token, chat_id) - def get_my_commands(self) -> List[types.BotCommand]: - """ - Use this method to get the current list of the bot's commands. + def get_my_commands(self, + scope: Optional[Union[ + types.BotCommandScopeDefault, types.BotCommandScopeAllPrivateChats, + types.BotCommandScopeAllGroupChats, types.BotCommandScopeAllChatAdministrators, + types.BotCommandScopeChat, + types.BotCommandScopeChatAdministrators, types.BotCommandScopeChatMember]]=None, + language_code: Optional[str]=None) -> List[types.BotCommand]: + """ + Use this method to get the current list of the bot's commands for the given scope and user language + :param scope: scope of users for which the commands are relevant + :param language_code: A two-letter ISO 639-1 language code Returns List of BotCommand on success. """ - result = apihelper.get_my_commands(self.token) + result = apihelper.get_my_commands(self.token, scope, language_code) return [types.BotCommand.de_json(cmd) for cmd in result] - def set_my_commands(self, commands: List[types.BotCommand]) -> bool: + def set_my_commands(self, commands: List[types.BotCommand], + scope: Optional[Union[ + types.BotCommandScopeDefault, types.BotCommandScopeAllPrivateChats, + types.BotCommandScopeAllGroupChats, types.BotCommandScopeAllChatAdministrators, + types.BotCommandScopeChat, + types.BotCommandScopeChatAdministrators, types.BotCommandScopeChatMember]] = None, + language_code: Optional[str]=None) -> bool: """ Use this method to change the list of the bot's commands. :param commands: List of BotCommand. At most 100 commands can be specified. + :param scope: scope of users for which the commands are relevant + :param language_code: A two-letter ISO 639-1 language code + :return: + """ + return apihelper.set_my_commands(self.token, commands, scope, language_code) + + def delete_my_commands(self, + scope: Optional[Union[ + types.BotCommandScopeDefault, types.BotCommandScopeAllPrivateChats, + types.BotCommandScopeAllGroupChats, types.BotCommandScopeAllChatAdministrators, + types.BotCommandScopeChat, + types.BotCommandScopeChatAdministrators, types.BotCommandScopeChatMember]]=None, + language_code: Optional[str]=None) -> bool: + """ + Use this method to delete the list of the bot's commands for the given scope and user language. + :param scope: scope of users for which the commands are relevant + :param language_code: A two-letter ISO 639-1 language code :return: """ - return apihelper.set_my_commands(self.token, commands) + return apihelper.delete_my_commands(self.token, scope, language_code) def set_chat_title(self, chat_id: Union[int, str], title: str) -> bool: """ diff --git a/telebot/apihelper.py b/telebot/apihelper.py index 867eef874..384c0bc1e 100644 --- a/telebot/apihelper.py +++ b/telebot/apihelper.py @@ -169,11 +169,6 @@ def get_me(token): return _make_request(token, method_url) -def get_my_commands(token): - method_url = r'getMyCommands' - return _make_request(token, method_url) - - def log_out(token): method_url = r'logOut' return _make_request(token, method_url) @@ -1032,9 +1027,33 @@ def set_chat_title(token, chat_id, title): return _make_request(token, method_url, params=payload, method='post') -def set_my_commands(token, commands): +def get_my_commands(token, scope, language_code): + method_url = r'getMyCommands' + payload = {} + if scope is not None: + payload['scope'] = scope.to_json() + if language_code is not None: + payload['language_code'] = language_code + return _make_request(token, method_url, params=payload, method='post') + + +def set_my_commands(token, commands, scope, language_code): method_url = r'setMyCommands' payload = {'commands': _convert_list_json_serializable(commands)} + if scope is not None: + payload['scope'] = scope.to_json() + if language_code is not None: + payload['language_code'] = language_code + return _make_request(token, method_url, params=payload, method='post') + + +def delete_my_commands(token, scope, language_code): + method_url = r'deleteMyCommands' + payload = {} + if scope is not None: + payload['scope'] = scope.to_json() + if language_code is not None: + payload['language_code'] = language_code return _make_request(token, method_url, params=payload, method='post') diff --git a/telebot/types.py b/telebot/types.py index ad7e4d302..19a6e0fe7 100644 --- a/telebot/types.py +++ b/telebot/types.py @@ -848,13 +848,16 @@ def __init__(self, file_id, file_unique_id, file_size, file_path, **kwargs): class ForceReply(JsonSerializable): - def __init__(self, selective=None): + def __init__(self, selective=None, input_field_placeholder=None): self.selective: bool = selective + self.input_field_placeholder = input_field_placeholder def to_json(self): json_dict = {'force_reply': True} if self.selective: json_dict['selective'] = True + if self.input_field_placeholder: + json_dict['input_field_placeholder'] = self.input_field_placeholder return json.dumps(json_dict) @@ -872,7 +875,8 @@ def to_json(self): class ReplyKeyboardMarkup(JsonSerializable): max_row_keys = 12 - def __init__(self, resize_keyboard=None, one_time_keyboard=None, selective=None, row_width=3): + def __init__(self, resize_keyboard=None, one_time_keyboard=None, selective=None, row_width=3, + input_field_placeholder=None): if row_width > self.max_row_keys: # Todo: Will be replaced with Exception in future releases if not DISABLE_KEYLEN_ERROR: @@ -883,6 +887,7 @@ def __init__(self, resize_keyboard=None, one_time_keyboard=None, selective=None, self.one_time_keyboard: bool = one_time_keyboard self.selective: bool = selective self.row_width: int = row_width + self.input_field_placeholder = input_field_placeholder self.keyboard: List[List[KeyboardButton]] = [] def add(self, *args, row_width=None): @@ -926,7 +931,7 @@ def row(self, *args): :param args: strings :return: self, to allow function chaining. """ - + return self.add(*args, row_width=self.max_row_keys) def to_json(self): @@ -942,6 +947,8 @@ def to_json(self): json_dict['resize_keyboard'] = True if self.selective: json_dict['selective'] = True + if self.input_field_placeholder: + json_dict['input_field_placeholder'] = self.input_field_placeholder return json.dumps(json_dict) @@ -1270,6 +1277,58 @@ def to_dict(self): return {'command': self.command, 'description': self.description} +# BotCommandScopes + +class BotCommandScope(JsonSerializable): + def __init__(self, type='default', chat_id=None, user_id=None): + self.type: str = type + self.chat_id: Optional[Union[int, str]] = chat_id + self.user_id: Optional[Union[int, str]] = user_id + + def to_json(self): + json_dict = {'type': self.type} + if self.chat_id: + json_dict['chat_id'] = self.chat_id + if self.user_id: + json_dict['user_id'] = self.user_id + return json.dumps(json_dict) + + +class BotCommandScopeDefault(BotCommandScope): + def __init__(self): + super(BotCommandScopeDefault, self).__init__(type='default') + + +class BotCommandScopeAllPrivateChats(BotCommandScope): + def __init__(self): + super(BotCommandScopeAllPrivateChats, self).__init__(type='all_private_chats') + + +class BotCommandScopeAllGroupChats(BotCommandScope): + def __init__(self): + super(BotCommandScopeAllGroupChats, self).__init__(type='all_group_chats') + + +class BotCommandScopeAllChatAdministrators(BotCommandScope): + def __init__(self): + super(BotCommandScopeAllChatAdministrators, self).__init__(type='all_chat_administrators') + + +class BotCommandScopeChat(BotCommandScope): + def __init__(self, chat_id=None): + super(BotCommandScopeChat, self).__init__(type='chat', chat_id=chat_id) + + +class BotCommandScopeChatAdministrators(BotCommandScope): + def __init__(self, chat_id=None): + super(BotCommandScopeChatAdministrators, self).__init__(type='chat_administrators', chat_id=chat_id) + + +class BotCommandScopeChatMember(BotCommandScope): + def __init__(self, chat_id=None, user_id=None): + super(BotCommandScopeChatMember, self).__init__(type='chat_administrators', chat_id=chat_id, user_id=user_id) + + # InlineQuery class InlineQuery(JsonDeserializable): diff --git a/tests/test_telebot.py b/tests/test_telebot.py index a22adcda0..a2f3d36e8 100644 --- a/tests/test_telebot.py +++ b/tests/test_telebot.py @@ -546,6 +546,24 @@ def test_send_document_formating_caption(self): ret_msg = tb.send_document(CHAT_ID, file_data, caption='_italic_', parse_mode='Markdown') assert ret_msg.caption_entities[0].type == 'italic' + def test_chat_commands(self): + tb = telebot.TeleBot(TOKEN) + command, description, lang = 'command_1', 'description of command 1', 'en' + scope = telebot.types.BotCommandScopeChat(CHAT_ID) + ret_msg = tb.set_my_commands([telebot.types.BotCommand(command, description)], scope, lang) + assert ret_msg is True + + ret_msg = tb.get_my_commands(scope, lang) + assert ret_msg[0].command == command + assert ret_msg[0].description == description + + ret_msg = tb.delete_my_commands(scope, lang) + assert ret_msg is True + + ret_msg = tb.get_my_commands(scope, lang) + assert ret_msg == [] + + def test_typed_middleware_handler(self): from telebot import apihelper From 491cc05a95c3b590e9a0ec6a9fdee42ffca74a97 Mon Sep 17 00:00:00 2001 From: MAIKS1900 Date: Sun, 27 Jun 2021 17:28:11 +0300 Subject: [PATCH 2/2] - Set BotCommandScope as abstract class. - Docstrings from telegram API Scope types --- telebot/types.py | 36 +++++++++++++++++++++++++++++++++++- 1 file changed, 35 insertions(+), 1 deletion(-) diff --git a/telebot/types.py b/telebot/types.py index 19a6e0fe7..8688132a1 100644 --- a/telebot/types.py +++ b/telebot/types.py @@ -2,6 +2,7 @@ import logging from typing import Dict, List, Optional, Union +from abc import ABC try: import ujson as json @@ -1279,8 +1280,19 @@ def to_dict(self): # BotCommandScopes -class BotCommandScope(JsonSerializable): +class BotCommandScope(ABC, JsonSerializable): def __init__(self, type='default', chat_id=None, user_id=None): + """ + Abstract class. + Use BotCommandScopeX classes to set a specific scope type: + BotCommandScopeDefault + BotCommandScopeAllPrivateChats + BotCommandScopeAllGroupChats + BotCommandScopeAllChatAdministrators + BotCommandScopeChat + BotCommandScopeChatAdministrators + BotCommandScopeChatMember + """ self.type: str = type self.chat_id: Optional[Union[int, str]] = chat_id self.user_id: Optional[Union[int, str]] = user_id @@ -1296,21 +1308,34 @@ def to_json(self): class BotCommandScopeDefault(BotCommandScope): def __init__(self): + """ + Represents the default scope of bot commands. + Default commands are used if no commands with a narrower scope are specified for the user. + """ super(BotCommandScopeDefault, self).__init__(type='default') class BotCommandScopeAllPrivateChats(BotCommandScope): def __init__(self): + """ + Represents the scope of bot commands, covering all private chats. + """ super(BotCommandScopeAllPrivateChats, self).__init__(type='all_private_chats') class BotCommandScopeAllGroupChats(BotCommandScope): def __init__(self): + """ + Represents the scope of bot commands, covering all group and supergroup chats. + """ super(BotCommandScopeAllGroupChats, self).__init__(type='all_group_chats') class BotCommandScopeAllChatAdministrators(BotCommandScope): def __init__(self): + """ + Represents the scope of bot commands, covering all group and supergroup chat administrators. + """ super(BotCommandScopeAllChatAdministrators, self).__init__(type='all_chat_administrators') @@ -1321,11 +1346,20 @@ def __init__(self, chat_id=None): class BotCommandScopeChatAdministrators(BotCommandScope): def __init__(self, chat_id=None): + """ + Represents the scope of bot commands, covering a specific chat. + @param chat_id: Unique identifier for the target chat + """ super(BotCommandScopeChatAdministrators, self).__init__(type='chat_administrators', chat_id=chat_id) class BotCommandScopeChatMember(BotCommandScope): def __init__(self, chat_id=None, user_id=None): + """ + Represents the scope of bot commands, covering all administrators of a specific group or supergroup chat + @param chat_id: Unique identifier for the target chat + @param user_id: Unique identifier of the target user + """ super(BotCommandScopeChatMember, self).__init__(type='chat_administrators', chat_id=chat_id, user_id=user_id)