diff --git a/examples/asynchronous_telebot/date_time_example.py b/examples/asynchronous_telebot/date_time_example.py new file mode 100644 index 000000000..c5b9e109f --- /dev/null +++ b/examples/asynchronous_telebot/date_time_example.py @@ -0,0 +1,32 @@ +#!/usr/bin/python + +# This example shows how to send a message with the Bot API 9.5 'date_time' entity. +# Telegram clients will render the selected text range as a localized date and time. + +import asyncio +import time + +from telebot import types +from telebot.async_telebot import AsyncTeleBot + + +API_TOKEN = '' +bot = AsyncTeleBot(API_TOKEN) + + +@bot.message_handler(commands=['date_time']) +async def send_date_time(message): + unix_time = int(time.time()) + 3600 + text = 'Reminder' + entity = types.MessageEntity( + type='date_time', + offset=0, + length=len(text), + unix_time=unix_time, + date_time_format='Dt' + ) + + await bot.send_message(message.chat.id, text, entities=[entity]) + + +asyncio.run(bot.polling()) diff --git a/examples/asynchronous_telebot/set_chat_member_tag_example.py b/examples/asynchronous_telebot/set_chat_member_tag_example.py new file mode 100644 index 000000000..ecb306a4c --- /dev/null +++ b/examples/asynchronous_telebot/set_chat_member_tag_example.py @@ -0,0 +1,28 @@ +#!/usr/bin/python + +# This example shows how to assign a tag to a regular supergroup member. +# The bot must be an administrator with permission to manage tags. + +import asyncio + +from telebot.async_telebot import AsyncTeleBot + + +API_TOKEN = '' +CHAT_ID = -1001234567890 +USER_ID = 123456789 +TAG = 'support' + +bot = AsyncTeleBot(API_TOKEN) + + +async def main(): + await bot.set_chat_member_tag(CHAT_ID, USER_ID, TAG) + + member = await bot.get_chat_member(CHAT_ID, USER_ID) + print(member.tag) + + await bot.close_session() + + +asyncio.run(main()) diff --git a/examples/date_time_example.py b/examples/date_time_example.py new file mode 100644 index 000000000..c09f1e60e --- /dev/null +++ b/examples/date_time_example.py @@ -0,0 +1,31 @@ +#!/usr/bin/python + +# This example shows how to send a message with the Bot API 9.5 'date_time' entity. +# Telegram clients will render the selected text range as a localized date and time. + +import time + +import telebot +from telebot import types + + +API_TOKEN = '' +bot = telebot.TeleBot(API_TOKEN) + + +@bot.message_handler(commands=['date_time']) +def send_date_time(message): + unix_time = int(time.time()) + 3600 + text = 'Reminder' + entity = types.MessageEntity( + type='date_time', + offset=0, + length=len(text), + unix_time=unix_time, + date_time_format='Dt' + ) + + bot.send_message(message.chat.id, text, entities=[entity]) + + +bot.infinity_polling() diff --git a/examples/set_chat_member_tag_example.py b/examples/set_chat_member_tag_example.py new file mode 100644 index 000000000..716c3f9d8 --- /dev/null +++ b/examples/set_chat_member_tag_example.py @@ -0,0 +1,19 @@ +#!/usr/bin/python + +# This example shows how to assign a tag to a regular supergroup member. +# The bot must be an administrator with permission to manage tags. + +import telebot + + +API_TOKEN = '' +CHAT_ID = -1001234567890 +USER_ID = 123456789 +TAG = 'support' + +bot = telebot.TeleBot(API_TOKEN) + +bot.set_chat_member_tag(CHAT_ID, USER_ID, TAG) + +member = bot.get_chat_member(CHAT_ID, USER_ID) +print(member.tag) diff --git a/telebot/__init__.py b/telebot/__init__.py index 9a8904a18..2e0ac468d 100644 --- a/telebot/__init__.py +++ b/telebot/__init__.py @@ -4236,8 +4236,8 @@ def send_message_draft( parse_mode: Optional[str]=None, entities: Optional[List[types.MessageEntity]]=None): """ - Use this method to stream a partial message to a user while the message is being generated; - supported only for bots with forum topic mode enabled. Returns True on success. + Use this method to stream a partial message to a user while the message is being generated. + Returns True on success. Telegram documentation: https://core.telegram.org/bots/api#sendmessagedraft @@ -4487,7 +4487,8 @@ def promote_chat_member( can_post_stories: Optional[bool]=None, can_edit_stories: Optional[bool]=None, can_delete_stories: Optional[bool]=None, - can_manage_direct_messages: Optional[bool]=None) -> bool: + can_manage_direct_messages: Optional[bool]=None, + can_manage_tags: Optional[bool]=None) -> bool: """ Use this method to promote or demote a user in a supergroup or a channel. The bot must be an administrator in the chat for this to work and must have the appropriate admin rights. @@ -4561,6 +4562,10 @@ def promote_chat_member( within the channel and decline suggested posts; for channels only :type can_manage_direct_messages: :obj:`bool` + :param can_manage_tags: Pass True if the administrator can edit the tags of regular members; + for groups and supergroups only + :type can_manage_tags: :obj:`bool` + :return: True on success. :rtype: :obj:`bool` """ @@ -4578,6 +4583,7 @@ def promote_chat_member( can_manage_video_chats=can_manage_video_chats, can_manage_topics=can_manage_topics, can_post_stories=can_post_stories, can_edit_stories=can_edit_stories, can_delete_stories=can_delete_stories, can_manage_direct_messages=can_manage_direct_messages, + can_manage_tags=can_manage_tags, ) @@ -4606,6 +4612,31 @@ def set_chat_administrator_custom_title( return apihelper.set_chat_administrator_custom_title(self.token, chat_id, user_id, custom_title) + def set_chat_member_tag( + self, chat_id: Union[int, str], user_id: int, tag: Optional[str]=None) -> bool: + """ + Use this method to set a tag for a regular member in a group or a supergroup. + The bot must be an administrator in the chat for this to work and must have the can_manage_tags administrator right. + Returns True on success. + + Telegram documentation: https://core.telegram.org/bots/api#setchatmembertag + + :param chat_id: Unique identifier for the target chat or username of the target supergroup + (in the format @supergroupusername) + :type chat_id: :obj:`int` or :obj:`str` + + :param user_id: Unique identifier of the target user + :type user_id: :obj:`int` + + :param tag: New tag for the member; 0-16 characters, emoji are not allowed + :type tag: :obj:`str` + + :return: True on success. + :rtype: :obj:`bool` + """ + return apihelper.set_chat_member_tag(self.token, chat_id, user_id, tag) + + def ban_chat_sender_chat(self, chat_id: Union[int, str], sender_chat_id: Union[int, str]) -> bool: """ Use this method to ban a channel chat in a supergroup or a channel. diff --git a/telebot/apihelper.py b/telebot/apihelper.py index ea189b26b..daee29314 100644 --- a/telebot/apihelper.py +++ b/telebot/apihelper.py @@ -1287,7 +1287,7 @@ def promote_chat_member( can_restrict_members=None, can_pin_messages=None, can_promote_members=None, is_anonymous=None, can_manage_chat=None, can_manage_video_chats=None, can_manage_topics=None, can_post_stories=None, can_edit_stories=None, - can_delete_stories=None, can_manage_direct_messages=None): + can_delete_stories=None, can_manage_direct_messages=None, can_manage_tags=None): method_url = 'promoteChatMember' payload = {'chat_id': chat_id, 'user_id': user_id} if can_change_info is not None: @@ -1322,6 +1322,8 @@ def promote_chat_member( payload['can_delete_stories'] = can_delete_stories if can_manage_direct_messages is not None: payload['can_manage_direct_messages'] = can_manage_direct_messages + if can_manage_tags is not None: + payload['can_manage_tags'] = can_manage_tags return _make_request(token, method_url, params=payload, method='post') @@ -1333,6 +1335,14 @@ def set_chat_administrator_custom_title(token, chat_id, user_id, custom_title): return _make_request(token, method_url, params=payload, method='post') +def set_chat_member_tag(token, chat_id, user_id, tag=None): + method_url = 'setChatMemberTag' + payload = {'chat_id': chat_id, 'user_id': user_id} + if tag is not None: + payload['tag'] = tag + return _make_request(token, method_url, params=payload, method='post') + + def ban_chat_sender_chat(token, chat_id, sender_chat_id): method_url = 'banChatSenderChat' payload = {'chat_id': chat_id, 'sender_chat_id': sender_chat_id} diff --git a/telebot/async_telebot.py b/telebot/async_telebot.py index 1bdece11b..e0bd2c4bb 100644 --- a/telebot/async_telebot.py +++ b/telebot/async_telebot.py @@ -5756,8 +5756,8 @@ async def send_message_draft( parse_mode: Optional[str]=None, entities: Optional[List[types.MessageEntity]]=None): """ - Use this method to stream a partial message to a user while the message is being generated; - supported only for bots with forum topic mode enabled. Returns True on success. + Use this method to stream a partial message to a user while the message is being generated. + Returns True on success. Telegram documentation: https://core.telegram.org/bots/api#sendmessagedraft @@ -5996,7 +5996,8 @@ async def promote_chat_member( can_post_stories: Optional[bool]=None, can_edit_stories: Optional[bool]=None, can_delete_stories: Optional[bool]=None, - can_manage_direct_messages: Optional[bool]=None) -> bool: + can_manage_direct_messages: Optional[bool]=None, + can_manage_tags: Optional[bool]=None) -> bool: """ Use this method to promote or demote a user in a supergroup or a channel. The bot must be an administrator in the chat for this to work and must have the appropriate admin rights. @@ -6070,6 +6071,10 @@ async def promote_chat_member( within the channel and decline suggested posts; for channels only :type can_manage_direct_messages: :obj:`bool` + :param can_manage_tags: Pass True if the administrator can edit the tags of regular members; + for groups and supergroups only + :type can_manage_tags: :obj:`bool` + :return: True on success. :rtype: :obj:`bool` """ @@ -6084,7 +6089,8 @@ async def promote_chat_member( can_edit_messages, can_delete_messages, can_invite_users, can_restrict_members, can_pin_messages, can_promote_members, is_anonymous, can_manage_chat, can_manage_video_chats, can_manage_topics, - can_post_stories, can_edit_stories, can_delete_stories, can_manage_direct_messages=can_manage_direct_messages + can_post_stories, can_edit_stories, can_delete_stories, + can_manage_direct_messages=can_manage_direct_messages, can_manage_tags=can_manage_tags ) async def set_chat_administrator_custom_title( @@ -6112,6 +6118,31 @@ async def set_chat_administrator_custom_title( return await asyncio_helper.set_chat_administrator_custom_title(self.token, chat_id, user_id, custom_title) + async def set_chat_member_tag( + self, chat_id: Union[int, str], user_id: int, tag: Optional[str]=None) -> bool: + """ + Use this method to set a tag for a regular member in a group or a supergroup. + The bot must be an administrator in the chat for this to work and must have the can_manage_tags administrator right. + Returns True on success. + + Telegram documentation: https://core.telegram.org/bots/api#setchatmembertag + + :param chat_id: Unique identifier for the target chat or username of the target supergroup + (in the format @supergroupusername) + :type chat_id: :obj:`int` or :obj:`str` + + :param user_id: Unique identifier of the target user + :type user_id: :obj:`int` + + :param tag: New tag for the member; 0-16 characters, emoji are not allowed + :type tag: :obj:`str` + + :return: True on success. + :rtype: :obj:`bool` + """ + return await asyncio_helper.set_chat_member_tag(self.token, chat_id, user_id, tag) + + async def ban_chat_sender_chat(self, chat_id: Union[int, str], sender_chat_id: Union[int, str]) -> bool: """ Use this method to ban a channel chat in a supergroup or a channel. diff --git a/telebot/asyncio_helper.py b/telebot/asyncio_helper.py index 48dd76a29..e3689a2ef 100644 --- a/telebot/asyncio_helper.py +++ b/telebot/asyncio_helper.py @@ -1281,7 +1281,7 @@ async def promote_chat_member( can_restrict_members=None, can_pin_messages=None, can_promote_members=None, is_anonymous=None, can_manage_chat=None, can_manage_video_chats=None, can_manage_topics=None, can_post_stories=None, can_edit_stories=None, can_delete_stories=None, - can_manage_direct_messages=None): + can_manage_direct_messages=None, can_manage_tags=None): method_url = 'promoteChatMember' payload = {'chat_id': chat_id, 'user_id': user_id} if can_change_info is not None: @@ -1316,6 +1316,8 @@ async def promote_chat_member( payload['can_delete_stories'] = can_delete_stories if can_manage_direct_messages is not None: payload['can_manage_direct_messages'] = can_manage_direct_messages + if can_manage_tags is not None: + payload['can_manage_tags'] = can_manage_tags return await _process_request(token, method_url, params=payload, method='post') @@ -1327,6 +1329,14 @@ async def set_chat_administrator_custom_title(token, chat_id, user_id, custom_ti return await _process_request(token, method_url, params=payload, method='post') +async def set_chat_member_tag(token, chat_id, user_id, tag=None): + method_url = 'setChatMemberTag' + payload = {'chat_id': chat_id, 'user_id': user_id} + if tag is not None: + payload['tag'] = tag + return await _process_request(token, method_url, params=payload, method='post') + + async def ban_chat_sender_chat(token, chat_id, sender_chat_id): method_url = 'banChatSenderChat' payload = {'chat_id': chat_id, 'sender_chat_id': sender_chat_id} @@ -2784,4 +2794,3 @@ class RequestTimeout(Exception): """ pass - diff --git a/telebot/types.py b/telebot/types.py index df8af2921..d2ee94c20 100644 --- a/telebot/types.py +++ b/telebot/types.py @@ -984,6 +984,9 @@ class Message(JsonDeserializable): :param sender_business_bot info: Optional. Information about the business bot that sent the message :type sender_business_bot_info: :class:`telebot.types.User` + :param sender_tag: Optional. Tag or custom title of the sender of the message; for supergroups only + :type sender_tag: :obj:`str` + :param date: Date the message was sent in Unix time :type date: :obj:`int` @@ -1534,6 +1537,8 @@ def de_json(cls, json_string): opts['reply_to_story'] = Story.de_json(obj['reply_to_story']) if 'sender_business_bot' in obj: opts['sender_business_bot'] = User.de_json(obj['sender_business_bot']) + if 'sender_tag' in obj: + opts['sender_tag'] = obj['sender_tag'] if 'business_connection_id' in obj: opts['business_connection_id'] = obj['business_connection_id'] if 'is_from_offline' in obj: @@ -1707,6 +1712,7 @@ def __init__(self, message_id, from_user, date, chat, content_type, options, jso self.sender_boost_count: Optional[int] = None self.reply_to_story: Optional[Story] = None self.sender_business_bot: Optional[User] = None + self.sender_tag: Optional[str] = None self.business_connection_id: Optional[str] = None self.is_from_offline: Optional[bool] = None self.effect_id: Optional[str] = None @@ -1859,7 +1865,7 @@ class MessageEntity(Dictionaryable, JsonSerializable, JsonDeserializable): “bot_command” (/start@jobs_bot), “url” (https://telegram.org), “email” (do-not-reply@telegram.org), “phone_number” (+1-212-555-0123), “bold” (bold text), “italic” (italic text), “underline” (underlined text), “strikethrough” (strikethrough text), “spoiler” (spoiler message), “blockquote” (block quotation), “expandable_blockquote” (collapsed-by-default block quotation), “code” (monowidth string), “pre” (monowidth block), “text_link” (for clickable text URLs), - “text_mention” (for users without usernames), “custom_emoji” (for inline custom emoji stickers) + “text_mention” (for users without usernames), “custom_emoji” (for inline custom emoji stickers), “date_time” (for formatted date and time) :type type: :obj:`str` :param offset: Offset in UTF-16 code units to the start of the entity @@ -1881,6 +1887,12 @@ class MessageEntity(Dictionaryable, JsonSerializable, JsonDeserializable): Use get_custom_emoji_stickers to get full information about the sticker. :type custom_emoji_id: :obj:`str` + :param unix_time: Optional. For “date_time” only, the Unix time associated with the entity + :type unix_time: :obj:`int` + + :param date_time_format: Optional. For “date_time” only, the string that defines the formatting of the date and time + :type date_time_format: :obj:`str` + :return: Instance of the class :rtype: :class:`telebot.types.MessageEntity` """ @@ -1904,7 +1916,8 @@ def de_json(cls, json_string): obj['user'] = User.de_json(obj['user']) return cls(**obj) - def __init__(self, type, offset, length, url=None, user=None, language=None, custom_emoji_id=None, **kwargs): + def __init__(self, type, offset, length, url=None, user=None, language=None, custom_emoji_id=None, + unix_time=None, date_time_format=None, **kwargs): self.type: str = type self.offset: int = offset self.length: int = length @@ -1912,6 +1925,8 @@ def __init__(self, type, offset, length, url=None, user=None, language=None, cus self.user: User = user self.language: str = language self.custom_emoji_id: Optional[str] = custom_emoji_id + self.unix_time: Optional[int] = unix_time + self.date_time_format: Optional[str] = date_time_format def to_json(self): return json.dumps(self.to_dict()) @@ -1923,7 +1938,9 @@ def to_dict(self): "url": self.url, "user": self.user.to_dict() if self.user else None, "language": self.language, - "custom_emoji_id": self.custom_emoji_id} + "custom_emoji_id": self.custom_emoji_id, + "unix_time": self.unix_time, + "date_time_format": self.date_time_format} class Dice(JsonSerializable, Dictionaryable, JsonDeserializable): @@ -3555,6 +3572,9 @@ class ChatMemberAdministrator(ChatMember): :param can_manage_direct_messages: Optional. True, if the administrator can manage direct messages of the channel and decline suggested posts; for channels only :type can_manage_direct_messages: :obj:`bool` + :param can_manage_tags: Optional. True, if the administrator can edit the tags of regular members; for groups and supergroups only + :type can_manage_tags: :obj:`bool` + :param custom_title: Optional. Custom title for this user :type custom_title: :obj:`str` @@ -3564,7 +3584,8 @@ class ChatMemberAdministrator(ChatMember): def __init__(self, user, status, can_be_edited, is_anonymous, can_manage_chat, can_delete_messages, can_manage_video_chats, can_restrict_members, can_promote_members, can_change_info, can_invite_users, can_post_stories, can_edit_stories, can_delete_stories, can_post_messages=None, can_edit_messages=None, - can_pin_messages=None, can_manage_topics=None, custom_title=None, can_manage_direct_messages=None, **kwargs): + can_pin_messages=None, can_manage_topics=None, custom_title=None, can_manage_direct_messages=None, + can_manage_tags=None, **kwargs): super().__init__(user, status, **kwargs) self.can_be_edited: bool = can_be_edited self.is_anonymous: bool = is_anonymous @@ -3584,6 +3605,7 @@ def __init__(self, user, status, can_be_edited, is_anonymous, can_manage_chat, c self.can_manage_topics: Optional[bool] = can_manage_topics self.custom_title: Optional[str] = custom_title self.can_manage_direct_messages: Optional[bool] = can_manage_direct_messages + self.can_manage_tags: Optional[bool] = can_manage_tags @property def can_manage_voice_chats(self): @@ -3604,15 +3626,19 @@ class ChatMemberMember(ChatMember): :param user: Information about the user :type user: :class:`telebot.types.User` + :param tag: Optional. Tag of the member + :type tag: :obj:`str` + :param until_date: Optional. Date when the user's subscription will expire; Unix time. If 0, then the user is a member forever :type until_date: :obj:`int` :return: Instance of the class :rtype: :class:`telebot.types.ChatMemberMember` """ - def __init__(self, user, status, until_date=None, **kwargs): + def __init__(self, user, status, until_date=None, tag=None, **kwargs): super().__init__(user, status, **kwargs) self.until_date: Optional[int] = until_date + self.tag: Optional[str] = tag # noinspection PyUnresolvedReferences @@ -3628,6 +3654,9 @@ class ChatMemberRestricted(ChatMember): :param user: Information about the user :type user: :class:`telebot.types.User` + :param tag: Optional. Tag of the member + :type tag: :obj:`str` + :param is_member: True, if the user is a member of the chat at the moment of the request :type is_member: :obj:`bool` @@ -3661,6 +3690,9 @@ class ChatMemberRestricted(ChatMember): :param can_add_web_page_previews: True, if the user is allowed to add web page previews to their messages :type can_add_web_page_previews: :obj:`bool` + :param can_edit_tag: True, if the user is allowed to edit their own tag + :type can_edit_tag: :obj:`bool` + :param can_change_info: True, if the user is allowed to change the chat title, photo and other settings :type can_change_info: :obj:`bool` @@ -3683,8 +3715,9 @@ def __init__(self, user, status, is_member, can_send_messages, can_send_audios, can_send_photos, can_send_videos, can_send_video_notes, can_send_voice_notes, can_send_polls, can_send_other_messages, can_add_web_page_previews, can_change_info, can_invite_users, can_pin_messages, can_manage_topics, - until_date=None, **kwargs): + until_date=None, tag=None, can_edit_tag=None, **kwargs): super().__init__(user, status, **kwargs) + self.tag: Optional[str] = tag self.is_member: bool = is_member self.can_send_messages: bool = can_send_messages self.can_send_audios: bool = can_send_audios @@ -3696,6 +3729,7 @@ def __init__(self, user, status, is_member, can_send_messages, can_send_audios, self.can_send_polls: bool = can_send_polls self.can_send_other_messages: bool = can_send_other_messages self.can_add_web_page_previews: bool = can_add_web_page_previews + self.can_edit_tag: Optional[bool] = can_edit_tag self.can_change_info: bool = can_change_info self.can_invite_users: bool = can_invite_users self.can_pin_messages: bool = can_pin_messages @@ -3784,6 +3818,9 @@ class ChatPermissions(JsonDeserializable, JsonSerializable, Dictionaryable): messages :type can_add_web_page_previews: :obj:`bool` + :param can_edit_tag: Optional. True, if the user is allowed to edit their own tag + :type can_edit_tag: :obj:`bool` + :param can_change_info: Optional. True, if the user is allowed to change the chat title, photo and other settings. Ignored in public supergroups :type can_change_info: :obj:`bool` @@ -3816,11 +3853,12 @@ def __init__(self, can_send_messages=None, can_send_media_messages=None,can_send can_send_voice_notes=None, can_send_polls=None, can_send_other_messages=None, can_add_web_page_previews=None, can_change_info=None, can_invite_users=None, can_pin_messages=None, - can_manage_topics=None, **kwargs): + can_manage_topics=None, can_edit_tag=None, **kwargs): self.can_send_messages: Optional[bool] = can_send_messages self.can_send_polls: Optional[bool] = can_send_polls self.can_send_other_messages: Optional[bool] = can_send_other_messages self.can_add_web_page_previews: Optional[bool] = can_add_web_page_previews + self.can_edit_tag: Optional[bool] = can_edit_tag self.can_change_info: Optional[bool] = can_change_info self.can_invite_users: Optional[bool] = can_invite_users self.can_pin_messages: Optional[bool] = can_pin_messages @@ -3870,6 +3908,8 @@ def to_dict(self): json_dict['can_send_other_messages'] = self.can_send_other_messages if self.can_add_web_page_previews is not None: json_dict['can_add_web_page_previews'] = self.can_add_web_page_previews + if self.can_edit_tag is not None: + json_dict['can_edit_tag'] = self.can_edit_tag if self.can_change_info is not None: json_dict['can_change_info'] = self.can_change_info if self.can_invite_users is not None: @@ -8053,6 +8093,9 @@ class ChatAdministratorRights(JsonDeserializable, JsonSerializable, Dictionaryab :param can_manage_direct_messages: Optional. True, if the administrator can manage direct messages of the channel and decline suggested posts; for channels only :type can_manage_direct_messages: :obj:`bool` + :param can_manage_tags: Optional. True, if the administrator can edit the tags of regular members; for groups and supergroups only + :type can_manage_tags: :obj:`bool` + :return: Instance of the class :rtype: :class:`telebot.types.ChatAdministratorRights` """ @@ -8070,6 +8113,7 @@ def __init__(self, is_anonymous: bool, can_manage_chat: bool, can_pin_messages: Optional[bool]=None, can_manage_topics: Optional[bool]=None, can_post_stories: Optional[bool]=None, can_edit_stories: Optional[bool]=None, can_delete_stories: Optional[bool]=None, can_manage_direct_messages: Optional[bool]=None, + can_manage_tags: Optional[bool]=None, **kwargs ) -> None: @@ -8089,6 +8133,7 @@ def __init__(self, is_anonymous: bool, can_manage_chat: bool, self.can_edit_stories: Optional[bool] = can_edit_stories self.can_delete_stories: Optional[bool] = can_delete_stories self.can_manage_direct_messages: Optional[bool] = can_manage_direct_messages + self.can_manage_tags: Optional[bool] = can_manage_tags def to_dict(self): json_dict = { @@ -8117,6 +8162,8 @@ def to_dict(self): json_dict['can_delete_stories'] = self.can_delete_stories if self.can_manage_direct_messages is not None: json_dict['can_manage_direct_messages'] = self.can_manage_direct_messages + if self.can_manage_tags is not None: + json_dict['can_manage_tags'] = self.can_manage_tags return json_dict diff --git a/tests/test_telebot.py b/tests/test_telebot.py index f395ff9c5..bc90a54d9 100644 --- a/tests/test_telebot.py +++ b/tests/test_telebot.py @@ -1,6 +1,7 @@ # -*- coding: utf-8 -*- import sys import warnings +import asyncio sys.path.append('../') @@ -14,6 +15,7 @@ from telebot import util should_skip = 'TOKEN' and 'CHAT_ID' not in os.environ +TEST_MEMBER_ID = os.environ.get('TEST_MEMBER_ID') if not should_skip: TOKEN = os.environ['TOKEN'] @@ -34,6 +36,90 @@ def deprecated2_new_function(): def deprecated2_old_function(): print("deprecated2_old_function") + +def test_set_chat_member_tag_forwards_to_apihelper(monkeypatch): + from telebot import apihelper + + captured = {} + bot = telebot.TeleBot('123:TEST') + + def fake_set_chat_member_tag(token, chat_id, user_id, tag=None): + captured['token'] = token + captured['chat_id'] = chat_id + captured['user_id'] = user_id + captured['tag'] = tag + return True + + monkeypatch.setattr(apihelper, 'set_chat_member_tag', fake_set_chat_member_tag) + + assert bot.set_chat_member_tag(-100123, 777, 'alpha') is True + assert captured == {'token': '123:TEST', 'chat_id': -100123, 'user_id': 777, 'tag': 'alpha'} + + +def test_promote_chat_member_forwards_can_manage_tags(monkeypatch): + from telebot import apihelper + + captured = {} + bot = telebot.TeleBot('123:TEST') + + def fake_promote_chat_member(token, chat_id, user_id, **kwargs): + captured['token'] = token + captured['chat_id'] = chat_id + captured['user_id'] = user_id + captured['kwargs'] = kwargs + return True + + monkeypatch.setattr(apihelper, 'promote_chat_member', fake_promote_chat_member) + + assert bot.promote_chat_member(-100123, 777, can_manage_tags=True) is True + assert captured['token'] == '123:TEST' + assert captured['chat_id'] == -100123 + assert captured['user_id'] == 777 + assert captured['kwargs']['can_manage_tags'] is True + + +def test_async_set_chat_member_tag_forwards_to_asyncio_helper(monkeypatch): + from telebot import asyncio_helper + from telebot.async_telebot import AsyncTeleBot + + captured = {} + bot = AsyncTeleBot('123:TEST') + + async def fake_set_chat_member_tag(token, chat_id, user_id, tag=None): + captured['token'] = token + captured['chat_id'] = chat_id + captured['user_id'] = user_id + captured['tag'] = tag + return True + + monkeypatch.setattr(asyncio_helper, 'set_chat_member_tag', fake_set_chat_member_tag) + + assert asyncio.run(bot.set_chat_member_tag(-100123, 777, 'alpha')) is True + assert captured == {'token': '123:TEST', 'chat_id': -100123, 'user_id': 777, 'tag': 'alpha'} + + +def test_async_promote_chat_member_forwards_can_manage_tags(monkeypatch): + from telebot import asyncio_helper + from telebot.async_telebot import AsyncTeleBot + + captured = {} + bot = AsyncTeleBot('123:TEST') + + async def fake_promote_chat_member(token, chat_id, user_id, *args, **kwargs): + captured['token'] = token + captured['chat_id'] = chat_id + captured['user_id'] = user_id + captured['kwargs'] = kwargs + return True + + monkeypatch.setattr(asyncio_helper, 'promote_chat_member', fake_promote_chat_member) + + assert asyncio.run(bot.promote_chat_member(-100123, 777, can_manage_tags=True)) is True + assert captured['token'] == '123:TEST' + assert captured['chat_id'] == -100123 + assert captured['user_id'] == 777 + assert captured['kwargs']['can_manage_tags'] is True + @pytest.mark.skipif(should_skip, reason="No environment variables configured") class TestTeleBot: def test_message_listener(self): @@ -271,6 +357,23 @@ def test_send_message(self): ret_msg = tb.send_message(CHAT_ID, text) assert ret_msg.message_id + def test_send_message_with_date_time_entity(self): + text = 'TIME' + unix_time = 1772945068 + tb = telebot.TeleBot(TOKEN) + entity = types.MessageEntity( + type='date_time', + offset=0, + length=len(text), + unix_time=unix_time, + date_time_format='Dt' + ) + ret_msg = tb.send_message(CHAT_ID, text, entities=[entity]) + assert ret_msg.message_id + assert ret_msg.entities[0].type == 'date_time' + assert ret_msg.entities[0].unix_time == unix_time + assert ret_msg.entities[0].date_time_format == 'Dt' + def test_send_dice(self): tb = telebot.TeleBot(TOKEN) ret_msg = tb.send_dice(CHAT_ID, emoji='🎯') @@ -433,6 +536,27 @@ def test_get_chat_members_count(self): cn = tb.get_chat_members_count(GROUP_ID) assert cn > 1 + @pytest.mark.skipif(TEST_MEMBER_ID is None, reason="No TEST_MEMBER_ID configured") + def test_set_chat_member_tag(self): + tb = telebot.TeleBot(TOKEN) + user_id = int(TEST_MEMBER_ID) + test_tag = 'ci95check' + member = tb.get_chat_member(GROUP_ID, user_id) + old_tag = getattr(member, 'tag', None) + if old_tag == test_tag: + test_tag = 'ci95smoke' + + assert member.status == 'member' + + try: + assert tb.set_chat_member_tag(GROUP_ID, user_id, test_tag) is True + updated_member = tb.get_chat_member(GROUP_ID, user_id) + assert updated_member.tag == test_tag + finally: + tb.set_chat_member_tag(GROUP_ID, user_id, old_tag) + restored_member = tb.get_chat_member(GROUP_ID, user_id) + assert getattr(restored_member, 'tag', None) == old_tag + def test_export_chat_invite_link(self): tb = telebot.TeleBot(TOKEN) il = tb.export_chat_invite_link(GROUP_ID) diff --git a/tests/test_types.py b/tests/test_types.py index 04e8b3ac8..a6b3023c7 100644 --- a/tests/test_types.py +++ b/tests/test_types.py @@ -251,6 +251,62 @@ def test_chat_member_updated(): assert cm_updated.new_chat_member.status == "left" +def test_message_entity_date_time(): + json_string = r'{"offset":0,"length":5,"type":"date_time","unix_time":1741392000,"date_time_format":"{date} {time}"}' + entity = types.MessageEntity.de_json(json_string) + assert entity.type == 'date_time' + assert entity.unix_time == 1741392000 + assert entity.date_time_format == '{date} {time}' + assert entity.to_dict()['unix_time'] == 1741392000 + assert entity.to_dict()['date_time_format'] == '{date} {time}' + + +def test_message_sender_tag(): + json_string = r'{"message_id":11,"from":{"id":108929734,"first_name":"Frank","last_name":"Wang","username":"eternnoir","is_bot":true},"chat":{"id":-1001734,"title":"Test","type":"supergroup"},"date":1741392000,"sender_tag":"Maintainer","text":"HIHI"}' + msg = types.Message.de_json(json_string) + assert msg.text == 'HIHI' + assert msg.sender_tag == 'Maintainer' + + +def test_chat_member_member_tag(): + json_string = r'{"user":{"id":77777777,"is_bot":false,"first_name":"Pepe"},"status":"member","tag":"alpha","until_date":0}' + member = types.ChatMember.de_json(json_string) + assert isinstance(member, types.ChatMemberMember) + assert member.tag == 'alpha' + assert member.until_date == 0 + + +def test_chat_member_restricted_tag_permissions(): + json_string = r'{"user":{"id":77777777,"is_bot":false,"first_name":"Pepe"},"status":"restricted","tag":"beta","is_member":true,"can_send_messages":true,"can_send_audios":true,"can_send_documents":true,"can_send_photos":true,"can_send_videos":true,"can_send_video_notes":true,"can_send_voice_notes":true,"can_send_polls":true,"can_send_other_messages":false,"can_add_web_page_previews":true,"can_edit_tag":true,"can_change_info":false,"can_invite_users":true,"can_pin_messages":false,"can_manage_topics":false,"until_date":1741392000}' + member = types.ChatMember.de_json(json_string) + assert isinstance(member, types.ChatMemberRestricted) + assert member.tag == 'beta' + assert member.can_edit_tag is True + + +def test_chat_member_admin_can_manage_tags(): + json_string = r'{"user":{"id":77777777,"is_bot":false,"first_name":"Pepe"},"status":"administrator","can_be_edited":true,"is_anonymous":false,"can_manage_chat":true,"can_delete_messages":true,"can_manage_video_chats":true,"can_restrict_members":true,"can_promote_members":true,"can_change_info":true,"can_invite_users":true,"can_post_stories":true,"can_edit_stories":true,"can_delete_stories":true,"can_manage_tags":true}' + member = types.ChatMember.de_json(json_string) + assert isinstance(member, types.ChatMemberAdministrator) + assert member.can_manage_tags is True + + +def test_chat_permissions_can_edit_tag(): + permissions = types.ChatPermissions(can_send_messages=True, can_edit_tag=True) + assert permissions.can_edit_tag is True + assert permissions.to_dict()['can_edit_tag'] is True + + +def test_chat_administrator_rights_can_manage_tags(): + rights = types.ChatAdministratorRights( + is_anonymous=False, can_manage_chat=True, can_delete_messages=True, + can_manage_video_chats=True, can_restrict_members=True, can_promote_members=True, + can_change_info=True, can_invite_users=True, can_manage_tags=True + ) + assert rights.can_manage_tags is True + assert rights.to_dict()['can_manage_tags'] is True + + def test_webhook_info(): json_string = r'{"url": "https://example.com/webhook", "has_custom_certificate": true, "pending_update_count": 1, "last_error_date": 0, "last_error_message": "", "last_synchronization_error_date": 489309, "max_connections": 40, "allowed_updates": ["message"]}' webhook_info = types.WebhookInfo.de_json(json_string)