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..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 @@ -848,13 +849,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 +876,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 +888,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 +932,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 +948,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 +1278,91 @@ def to_dict(self): return {'command': self.command, 'description': self.description} +# BotCommandScopes + +class BotCommandScope(ABC, JsonSerializable): + def __init__(self, type='default', chat_id=None, user_id=None): + """ + Abstract class. + Use BotCommandScopeX classes to set a specific scope type: + BotCommandScopeDefault + BotCommandScopeAllPrivateChats + BotCommandScopeAllGroupChats + BotCommandScopeAllChatAdministrators + BotCommandScopeChat + BotCommandScopeChatAdministrators + BotCommandScopeChatMember + """ + self.type: str = type + self.chat_id: Optional[Union[int, str]] = chat_id + self.user_id: Optional[Union[int, str]] = user_id + + def to_json(self): + json_dict = {'type': self.type} + if self.chat_id: + json_dict['chat_id'] = self.chat_id + if self.user_id: + json_dict['user_id'] = self.user_id + return json.dumps(json_dict) + + +class BotCommandScopeDefault(BotCommandScope): + def __init__(self): + """ + Represents the default scope of bot commands. + Default commands are used if no commands with a narrower scope are specified for the user. + """ + super(BotCommandScopeDefault, self).__init__(type='default') + + +class BotCommandScopeAllPrivateChats(BotCommandScope): + def __init__(self): + """ + Represents the scope of bot commands, covering all private chats. + """ + super(BotCommandScopeAllPrivateChats, self).__init__(type='all_private_chats') + + +class BotCommandScopeAllGroupChats(BotCommandScope): + def __init__(self): + """ + Represents the scope of bot commands, covering all group and supergroup chats. + """ + super(BotCommandScopeAllGroupChats, self).__init__(type='all_group_chats') + + +class BotCommandScopeAllChatAdministrators(BotCommandScope): + def __init__(self): + """ + Represents the scope of bot commands, covering all group and supergroup chat administrators. + """ + super(BotCommandScopeAllChatAdministrators, self).__init__(type='all_chat_administrators') + + +class BotCommandScopeChat(BotCommandScope): + def __init__(self, chat_id=None): + super(BotCommandScopeChat, self).__init__(type='chat', chat_id=chat_id) + + +class BotCommandScopeChatAdministrators(BotCommandScope): + def __init__(self, chat_id=None): + """ + Represents the scope of bot commands, covering a specific chat. + @param chat_id: Unique identifier for the target chat + """ + super(BotCommandScopeChatAdministrators, self).__init__(type='chat_administrators', chat_id=chat_id) + + +class BotCommandScopeChatMember(BotCommandScope): + def __init__(self, chat_id=None, user_id=None): + """ + Represents the scope of bot commands, covering all administrators of a specific group or supergroup chat + @param chat_id: Unique identifier for the target chat + @param user_id: Unique identifier of the target user + """ + super(BotCommandScopeChatMember, self).__init__(type='chat_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