diff --git a/docs/source/telegram.chatlocation.rst b/docs/source/telegram.chatlocation.rst new file mode 100644 index 00000000000..8994d138e2d --- /dev/null +++ b/docs/source/telegram.chatlocation.rst @@ -0,0 +1,6 @@ +telegram.ChatLocation +===================== + +.. autoclass:: telegram.ChatLocation + :members: + :show-inheritance: diff --git a/telegram/__init__.py b/telegram/__init__.py index 2efe106d4f6..57a9168655d 100644 --- a/telegram/__init__.py +++ b/telegram/__init__.py @@ -23,6 +23,7 @@ from .user import User from .files.chatphoto import ChatPhoto from .chat import Chat +from .chatlocation import ChatLocation from .chatmember import ChatMember from .chatpermissions import ChatPermissions from .files.photosize import PhotoSize @@ -190,6 +191,7 @@ 'InputTextMessageContent', 'InputVenueMessageContent', 'Location', + 'ChatLocation', 'ProximityAlertTriggered', 'EncryptedCredentials', 'PassportFile', diff --git a/telegram/bot.py b/telegram/bot.py index 80480033f13..d64c81ca912 100644 --- a/telegram/bot.py +++ b/telegram/bot.py @@ -2115,16 +2115,21 @@ def unban_chat_member( user_id: Union[str, int], timeout: float = None, api_kwargs: JSONDict = None, + only_if_banned: bool = None, ) -> bool: """Use this method to unban a previously kicked user in a supergroup or channel. - The user will not return to the group automatically, but will be able to join via link, - etc. The bot must be an administrator in the group for this to work. + The user will *not* return to the group or channel automatically, but will be able to join + via link, etc. The bot must be an administrator for this to work. By default, this method + guarantees that after the call the user is not a member of the chat, but will be able to + join it. So if the user is a member of the chat they will also be *removed* from the chat. + If you don't want this, use the parameter :attr:`only_if_banned`. Args: chat_id (:obj:`int` | :obj:`str`): Unique identifier for the target chat or username of the target channel (in the format @channelusername). user_id (:obj:`int`): Unique identifier of the target user. + only_if_banned (:obj:`bool`, optional): Do nothing if the user is not banned. timeout (:obj:`int` | :obj:`float`, optional): If this value is specified, use it as the read timeout from the server (instead of the one specified during creation of the connection pool). @@ -2140,6 +2145,9 @@ def unban_chat_member( """ data: JSONDict = {'chat_id': chat_id, 'user_id': user_id} + if only_if_banned is not None: + data['only_if_banned'] = only_if_banned + result = self._post('unbanChatMember', data, timeout=timeout, api_kwargs=api_kwargs) return result # type: ignore[return-value] diff --git a/telegram/chat.py b/telegram/chat.py index 442d4c9c0d5..129db272937 100644 --- a/telegram/chat.py +++ b/telegram/chat.py @@ -25,6 +25,7 @@ from telegram.utils.types import JSONDict from .chatpermissions import ChatPermissions +from .chatlocation import ChatLocation if TYPE_CHECKING: from telegram import Bot, ChatMember, Message @@ -44,6 +45,8 @@ class Chat(TelegramObject): first_name (:obj:`str`): Optional. First name of the other party in a private chat. last_name (:obj:`str`): Optional. Last name of the other party in a private chat. photo (:class:`telegram.ChatPhoto`): Optional. Chat photo. + bio (:obj:`str`): Optional. Bio of the other party in a private chat. Returned only in + :meth:`telegram.Bot.get_chat`. description (:obj:`str`): Optional. Description, for groups, supergroups and channel chats. invite_link (:obj:`str`): Optional. Chat invite link, for supergroups and channel chats. pinned_message (:class:`telegram.Message`): Optional. The most recent pinned message @@ -56,6 +59,11 @@ class Chat(TelegramObject): sticker_set_name (:obj:`str`): Optional. For supergroups, name of Group sticker set. can_set_sticker_set (:obj:`bool`): Optional. :obj:`True`, if the bot can change group the sticker set. + linked_chat_id (:obj:`int`): Optional. Unique identifier for the linked chat, i.e. the + discussion group identifier for a channel and vice versa; for supergroups and channel + chats. Returned only in :meth:`telegram.Bot.get_chat`. + location (:class:`telegram.ChatLocation`): Optional. For supergroups, the location to which + the supergroup is connected. Returned only in :meth:`telegram.Bot.get_chat`. Args: id (:obj:`int`): Unique identifier for this chat. This number may be greater than 32 bits @@ -71,6 +79,8 @@ class Chat(TelegramObject): last_name(:obj:`str`, optional): Last name of the other party in a private chat. photo (:class:`telegram.ChatPhoto`, optional): Chat photo. Returned only in :meth:`telegram.Bot.get_chat`. + bio (:obj:`str`, optional): Bio of the other party in a private chat. Returned only in + :meth:`telegram.Bot.get_chat`. description (:obj:`str`, optional): Description, for groups, supergroups and channel chats. Returned only in :meth:`telegram.Bot.get_chat`. invite_link (:obj:`str`, optional): Chat invite link, for groups, supergroups and channel @@ -89,6 +99,11 @@ class Chat(TelegramObject): Returned only in :meth:`telegram.Bot.get_chat`. can_set_sticker_set (:obj:`bool`, optional): :obj:`True`, if the bot can change group the sticker set. Returned only in :meth:`telegram.Bot.get_chat`. + linked_chat_id (:obj:`int`, optional): Unique identifier for the linked chat, i.e. the + discussion group identifier for a channel and vice versa; for supergroups and channel + chats. Returned only in :meth:`telegram.Bot.get_chat`. + location (:class:`telegram.ChatLocation`, optional): For supergroups, the location to which + the supergroup is connected. Returned only in :meth:`telegram.Bot.get_chat`. **kwargs (:obj:`dict`): Arbitrary keyword arguments. """ @@ -119,6 +134,9 @@ def __init__( sticker_set_name: str = None, can_set_sticker_set: bool = None, slow_mode_delay: int = None, + bio: str = None, + linked_chat_id: int = None, + location: ChatLocation = None, **_kwargs: Any, ): # Required @@ -132,6 +150,7 @@ def __init__( # TODO: Remove (also from tests), when Telegram drops this completely self.all_members_are_administrators = _kwargs.get('all_members_are_administrators') self.photo = photo + self.bio = bio self.description = description self.invite_link = invite_link self.pinned_message = pinned_message @@ -139,6 +158,8 @@ def __init__( self.slow_mode_delay = slow_mode_delay self.sticker_set_name = sticker_set_name self.can_set_sticker_set = can_set_sticker_set + self.linked_chat_id = linked_chat_id + self.location = location self.bot = bot self._id_attrs = (self.id,) @@ -163,6 +184,7 @@ def de_json(cls, data: JSONDict, bot: 'Bot') -> Optional['Chat']: data['pinned_message'] = Message.de_json(data.get('pinned_message'), bot) data['permissions'] = ChatPermissions.de_json(data.get('permissions'), bot) + data['location'] = ChatLocation.de_json(data.get('location'), bot) return cls(bot=bot, **data) diff --git a/telegram/chatlocation.py b/telegram/chatlocation.py new file mode 100644 index 00000000000..3dbe0516d90 --- /dev/null +++ b/telegram/chatlocation.py @@ -0,0 +1,70 @@ +#!/usr/bin/env python +# +# A library that provides a Python interface to the Telegram Bot API +# Copyright (C) 2015-2020 +# Leandro Toledo de Souza +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Lesser Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Lesser Public License for more details. +# +# You should have received a copy of the GNU Lesser Public License +# along with this program. If not, see [http://www.gnu.org/licenses/]. +"""This module contains an object that represents a location to which a chat is connected.""" + +from typing import TYPE_CHECKING, Any, Optional + +from telegram import TelegramObject +from telegram.utils.types import JSONDict + +from .files.location import Location + +if TYPE_CHECKING: + from telegram import Bot + + +class ChatLocation(TelegramObject): + """This object represents a location to which a chat is connected. + + Objects of this class are comparable in terms of equality. Two objects of this class are + considered equal, if their :attr:`location` is equal. + + Attributes: + location (:class:`telegram.Location`): The location to which the supergroup is connected. + address (:obj:`str`): Location address, as defined by the chat owner + + Args: + location (:class:`telegram.Location`): The location to which the supergroup is connected. + Can't be a live location. + address (:obj:`str`): Location address; 1-64 characters, as defined by the chat owner + **kwargs (:obj:`dict`): Arbitrary keyword arguments. + + """ + + def __init__( + self, + location: Location, + address: str, + **_kwargs: Any, + ): + self.location = location + self.address = address + + self._id_attrs = (self.location,) + + @classmethod + def de_json(cls, data: Optional[JSONDict], bot: 'Bot') -> Optional['ChatLocation']: + data = cls.parse_data(data) + + if not data: + return None + + data['location'] = Location.de_json(data.get('location'), bot) + + return cls(bot=bot, **data) diff --git a/tests/test_bot.py b/tests/test_bot.py index 24599e2dea1..bb636023adc 100644 --- a/tests/test_bot.py +++ b/tests/test_bot.py @@ -707,15 +707,17 @@ def test(url, data, *args, **kwargs): assert tz_bot.kick_chat_member(2, 32, until_date=until_timestamp) # TODO: Needs improvement. - def test_unban_chat_member(self, monkeypatch, bot): - def test(url, data, *args, **kwargs): + @pytest.mark.parametrize('only_if_banned', [True, False, None]) + def test_unban_chat_member(self, monkeypatch, bot, only_if_banned): + def make_assertion(url, data, *args, **kwargs): chat_id = data['chat_id'] == 2 user_id = data['user_id'] == 32 - return chat_id and user_id + o_i_b = data.get('only_if_banned', None) == only_if_banned + return chat_id and user_id and o_i_b - monkeypatch.setattr(bot.request, 'post', test) + monkeypatch.setattr(bot.request, 'post', make_assertion) - assert bot.unban_chat_member(2, 32) + assert bot.unban_chat_member(2, 32, only_if_banned=only_if_banned) def test_set_chat_permissions(self, monkeypatch, bot, chat_permissions): def test(url, data, *args, **kwargs): diff --git a/tests/test_chat.py b/tests/test_chat.py index bd7f8c24b42..f257b67de31 100644 --- a/tests/test_chat.py +++ b/tests/test_chat.py @@ -19,7 +19,7 @@ import pytest -from telegram import Chat, ChatAction, ChatPermissions +from telegram import Chat, ChatAction, ChatPermissions, ChatLocation, Location from telegram import User @@ -36,6 +36,9 @@ def chat(bot): can_set_sticker_set=TestChat.can_set_sticker_set, permissions=TestChat.permissions, slow_mode_delay=TestChat.slow_mode_delay, + bio=TestChat.bio, + linked_chat_id=TestChat.linked_chat_id, + location=TestChat.location, ) @@ -53,6 +56,9 @@ class TestChat: can_invite_users=True, ) slow_mode_delay = 30 + bio = "I'm a Barbie Girl in a Barbie World" + linked_chat_id = 11880 + location = ChatLocation(Location(123, 456), 'Barbie World') def test_de_json(self, bot): json_dict = { @@ -65,6 +71,9 @@ def test_de_json(self, bot): 'can_set_sticker_set': self.can_set_sticker_set, 'permissions': self.permissions.to_dict(), 'slow_mode_delay': self.slow_mode_delay, + 'bio': self.bio, + 'linked_chat_id': self.linked_chat_id, + 'location': self.location.to_dict(), } chat = Chat.de_json(json_dict, bot) @@ -77,6 +86,10 @@ def test_de_json(self, bot): assert chat.can_set_sticker_set == self.can_set_sticker_set assert chat.permissions == self.permissions assert chat.slow_mode_delay == self.slow_mode_delay + assert chat.bio == self.bio + assert chat.linked_chat_id == self.linked_chat_id + assert chat.location.location == self.location.location + assert chat.location.address == self.location.address def test_to_dict(self, chat): chat_dict = chat.to_dict() @@ -89,6 +102,9 @@ def test_to_dict(self, chat): assert chat_dict['all_members_are_administrators'] == chat.all_members_are_administrators assert chat_dict['permissions'] == chat.permissions.to_dict() assert chat_dict['slow_mode_delay'] == chat.slow_mode_delay + assert chat_dict['bio'] == chat.bio + assert chat_dict['linked_chat_id'] == chat.linked_chat_id + assert chat_dict['location'] == chat.location.to_dict() def test_link(self, chat): assert chat.link == 'https://t.me/{}'.format(chat.username) @@ -145,14 +161,16 @@ def test(*args, **kwargs): monkeypatch.setattr(chat.bot, 'kick_chat_member', test) assert chat.kick_member(42, until_date=43) - def test_unban_member(self, monkeypatch, chat): - def test(*args, **kwargs): + @pytest.mark.parametrize('only_if_banned', [True, False, None]) + def test_unban_member(self, monkeypatch, chat, only_if_banned): + def make_assertion(*args, **kwargs): chat_id = args[0] == chat.id user_id = args[1] == 42 - return chat_id and user_id + o_i_b = kwargs.get('only_if_banned', None) == only_if_banned + return chat_id and user_id and o_i_b - monkeypatch.setattr(chat.bot, 'unban_chat_member', test) - assert chat.unban_member(42) + monkeypatch.setattr(chat.bot, 'unban_chat_member', make_assertion) + assert chat.unban_member(42, only_if_banned=only_if_banned) def test_set_permissions(self, monkeypatch, chat): def test(*args, **kwargs): diff --git a/tests/test_chatlocation.py b/tests/test_chatlocation.py new file mode 100644 index 00000000000..f6a046d4237 --- /dev/null +++ b/tests/test_chatlocation.py @@ -0,0 +1,69 @@ +#!/usr/bin/env python +# +# A library that provides a Python interface to the Telegram Bot API +# Copyright (C) 2015-2020 +# Leandro Toledo de Souza +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Lesser Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Lesser Public License for more details. +# +# You should have received a copy of the GNU Lesser Public License +# along with this program. If not, see [http://www.gnu.org/licenses/]. + +import pytest + +from telegram import Location, ChatLocation, User + + +@pytest.fixture(scope='class') +def chat_location(bot): + return ChatLocation(TestChatLocation.location, TestChatLocation.address) + + +class TestChatLocation: + location = Location(123, 456) + address = 'The Shire' + + def test_de_json(self, bot): + json_dict = { + 'location': self.location.to_dict(), + 'address': self.address, + } + chat_location = ChatLocation.de_json(json_dict, bot) + + assert chat_location.location == self.location + assert chat_location.address == self.address + + def test_to_dict(self, chat_location): + chat_location_dict = chat_location.to_dict() + + assert isinstance(chat_location_dict, dict) + assert chat_location_dict['location'] == chat_location.location.to_dict() + assert chat_location_dict['address'] == chat_location.address + + def test_equality(self, chat_location): + a = chat_location + b = ChatLocation(self.location, self.address) + c = ChatLocation(self.location, 'Mordor') + d = ChatLocation(Location(456, 132), self.address) + e = User(456, '', False) + + assert a == b + assert hash(a) == hash(b) + assert a is not b + + assert a == c + assert hash(a) == hash(c) + + assert a != d + assert hash(a) != hash(d) + + assert a != e + assert hash(a) != hash(e)