diff --git a/README.rst b/README.rst index baf64845..50e848db 100644 --- a/README.rst +++ b/README.rst @@ -20,7 +20,9 @@ Pyrogram **Pyrogram** is an elegant, easy-to-use Telegram_ client library and framework written from the ground up in Python and C. It enables you to easily create custom apps using both user and bot identities (bot API alternative) via the `MTProto API`_. - `A fully-asynchronous variant is also available » `_ + `Pyrogram in fully-asynchronous mode is also available » `_ + + `Working PoC of Telegram voice calls using Pyrogram » `_ Features -------- diff --git a/compiler/error/source/400_BAD_REQUEST.tsv b/compiler/error/source/400_BAD_REQUEST.tsv index b7a629be..953b96af 100644 --- a/compiler/error/source/400_BAD_REQUEST.tsv +++ b/compiler/error/source/400_BAD_REQUEST.tsv @@ -88,5 +88,6 @@ MEDIA_INVALID The media is invalid BOT_SCORE_NOT_MODIFIED The bot score was not modified USER_BOT_REQUIRED The method can be used by bots only IMAGE_PROCESS_FAILED The server failed to process your image +USERNAME_NOT_MODIFIED The username was not modified CALL_ALREADY_ACCEPTED The call is already accepted CALL_ALREADY_DECLINED The call is already declined diff --git a/examples/hello.py b/examples/hello.py index 54e86812..19d0ffe7 100644 --- a/examples/hello.py +++ b/examples/hello.py @@ -13,4 +13,4 @@ with app: app.send_location("me", 51.500729, -0.124583) # Send a sticker - app.send_sticker("me", "CAADBAADhw4AAvLQYAHICbZ5SUs_jwI") + app.send_sticker("me", "CAADBAADyg4AAvLQYAEYD4F7vcZ43AI") diff --git a/pyrogram/client/client.py b/pyrogram/client/client.py index 267aade4..f58d3ceb 100644 --- a/pyrogram/client/client.py +++ b/pyrogram/client/client.py @@ -30,6 +30,7 @@ import shutil import struct import tempfile import time +import warnings from configparser import ConfigParser from datetime import datetime from hashlib import sha256, md5 @@ -68,10 +69,10 @@ class Client(Methods, BaseClient): Args: session_name (``str``): - Name to uniquely identify a session of either a User or a Bot. - For Users: pass a string of your choice, e.g.: "my_main_account". - For Bots: pass your Bot API token, e.g.: "123456:ABC-DEF1234ghIkl-zyx57W2v1u123ew11" - Note: as long as a valid User session file exists, Pyrogram won't ask you again to input your phone number. + Name to uniquely identify a session of either a User or a Bot, e.g.: "my_account". This name will be used + to save a file to disk that stores details needed for reconnecting without asking again for credentials. + Note for bots: You can pass a bot token here, but this usage will be deprecated in next releases. + Use *bot_token* instead. api_id (``int``, *optional*): The *api_id* part of your Telegram API Key, as integer. E.g.: 12345 @@ -145,6 +146,10 @@ class Client(Methods, BaseClient): a new Telegram account in case the phone number you passed is not registered yet. Only applicable for new sessions. + bot_token (``str``, *optional*): + Pass your Bot API token to create a bot session, e.g.: "123456:ABC-DEF1234ghIkl-zyx57W2v1u123ew11" + Only applicable for new sessions. + last_name (``str``, *optional*): Same purpose as *first_name*; pass a Last Name to avoid entering it manually. It can be an empty string: "". Only applicable for new sessions. @@ -193,6 +198,7 @@ class Client(Methods, BaseClient): password: str = None, recovery_code: callable = None, force_sms: bool = False, + bot_token: str = None, first_name: str = None, last_name: str = None, workers: int = BaseClient.WORKERS, @@ -219,6 +225,7 @@ class Client(Methods, BaseClient): self.password = password self.recovery_code = recovery_code self.force_sms = force_sms + self.bot_token = bot_token self.first_name = first_name self.last_name = last_name self.workers = workers @@ -264,8 +271,13 @@ class Client(Methods, BaseClient): raise ConnectionError("Client has already been started") if self.BOT_TOKEN_RE.match(self.session_name): + self.is_bot = True self.bot_token = self.session_name self.session_name = self.session_name.split(":")[0] + warnings.warn('\nYou are using a bot token as session name.\n' + 'It will be deprecated in next update, please use session file name to load ' + 'existing sessions and bot_token argument to create new sessions.', + DeprecationWarning, stacklevel=2) self.load_config() await self.load_session() @@ -283,13 +295,15 @@ class Client(Methods, BaseClient): try: if self.user_id is None: if self.bot_token is None: + self.is_bot = False await self.authorize_user() else: + self.is_bot = True await self.authorize_bot() self.save_session() - if self.bot_token is None: + if not self.is_bot: if self.takeout: self.takeout_id = (await self.send(functions.account.InitTakeoutSession())).id log.warning("Takeout session {} initiated".format(self.takeout_id)) @@ -1112,6 +1126,8 @@ class Client(Methods, BaseClient): self.auth_key = base64.b64decode("".join(s["auth_key"])) self.user_id = s["user_id"] self.date = s.get("date", 0) + # TODO: replace default with False once token session name will be deprecated + self.is_bot = s.get("is_bot", self.is_bot) for k, v in s.get("peers_by_id", {}).items(): self.peers_by_id[int(k)] = utils.get_input_peer(int(k), v) @@ -1138,7 +1154,7 @@ class Client(Methods, BaseClient): if include is None: for path in sorted(Path(root).rglob("*.py")): - module_path = os.path.splitext(str(path))[0].replace("/", ".") + module_path = '.'.join(path.parent.parts + (path.stem,)) module = import_module(module_path) for name in vars(module).keys(): @@ -1245,7 +1261,8 @@ class Client(Methods, BaseClient): test_mode=self.test_mode, auth_key=auth_key, user_id=self.user_id, - date=self.date + date=self.date, + is_bot=self.is_bot, ), f, indent=4 diff --git a/pyrogram/client/ext/base_client.py b/pyrogram/client/ext/base_client.py index d7414530..a99a92d0 100644 --- a/pyrogram/client/ext/base_client.py +++ b/pyrogram/client/ext/base_client.py @@ -67,7 +67,7 @@ class BaseClient: } def __init__(self): - self.bot_token = None + self.is_bot = None self.dc_id = None self.auth_key = None self.user_id = None diff --git a/pyrogram/client/ext/syncer.py b/pyrogram/client/ext/syncer.py index c07d1936..b9f90d26 100644 --- a/pyrogram/client/ext/syncer.py +++ b/pyrogram/client/ext/syncer.py @@ -92,6 +92,7 @@ class Syncer: auth_key=auth_key, user_id=client.user_id, date=int(time.time()), + is_bot=client.is_bot, peers_by_id={ k: getattr(v, "access_hash", None) for k, v in client.peers_by_id.copy().items() diff --git a/pyrogram/client/methods/chats/__init__.py b/pyrogram/client/methods/chats/__init__.py index 6cc034e4..961038a8 100644 --- a/pyrogram/client/methods/chats/__init__.py +++ b/pyrogram/client/methods/chats/__init__.py @@ -37,6 +37,7 @@ from .set_chat_photo import SetChatPhoto from .set_chat_title import SetChatTitle from .unban_chat_member import UnbanChatMember from .unpin_chat_message import UnpinChatMessage +from .update_chat_username import UpdateChatUsername class Chats( @@ -60,6 +61,7 @@ class Chats( GetChatMembersCount, GetChatPreview, IterDialogs, - IterChatMembers + IterChatMembers, + UpdateChatUsername ): pass diff --git a/pyrogram/client/methods/chats/join_chat.py b/pyrogram/client/methods/chats/join_chat.py index f3b5d19f..65300445 100644 --- a/pyrogram/client/methods/chats/join_chat.py +++ b/pyrogram/client/methods/chats/join_chat.py @@ -16,6 +16,7 @@ # You should have received a copy of the GNU Lesser General Public License # along with Pyrogram. If not, see . +import pyrogram from pyrogram.api import functions, types from ...ext import BaseClient @@ -30,17 +31,24 @@ class JoinChat(BaseClient): Unique identifier for the target chat in form of a *t.me/joinchat/* link or username of the target channel/supergroup (in the format @username). + Returns: + On success, a :obj:`Chat ` object is returned. + Raises: :class:`Error ` in case of a Telegram RPC error. """ match = self.INVITE_LINK_RE.match(chat_id) if match: - return await self.send( + chat = await self.send( functions.messages.ImportChatInvite( hash=match.group(1) ) ) + if isinstance(chat.chats[0], types.Chat): + return pyrogram.Chat._parse_chat_chat(self, chat.chats[0]) + elif isinstance(chat.chats[0], types.Channel): + return pyrogram.Chat._parse_channel_chat(self, chat.chats[0]) else: resolved_peer = await self.send( functions.contacts.ResolveUsername( @@ -53,8 +61,10 @@ class JoinChat(BaseClient): access_hash=resolved_peer.chats[0].access_hash ) - return await self.send( + chat = await self.send( functions.channels.JoinChannel( channel=channel ) ) + + return pyrogram.Chat._parse_channel_chat(self, chat.chats[0]) diff --git a/pyrogram/client/methods/chats/update_chat_username.py b/pyrogram/client/methods/chats/update_chat_username.py new file mode 100644 index 00000000..156ee6f8 --- /dev/null +++ b/pyrogram/client/methods/chats/update_chat_username.py @@ -0,0 +1,59 @@ +# Pyrogram - Telegram MTProto API Client Library for Python +# Copyright (C) 2017-2019 Dan Tès +# +# This file is part of Pyrogram. +# +# Pyrogram is free software: you can redistribute it and/or modify +# it under the terms of the GNU Lesser General Public License as published +# by the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Pyrogram 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 General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public License +# along with Pyrogram. If not, see . + +from typing import Union + +from pyrogram.api import functions, types +from ...ext import BaseClient + + +class UpdateChatUsername(BaseClient): + def update_chat_username(self, + chat_id: Union[int, str], + username: Union[str, None]) -> bool: + """Use this method to update a channel or a supergroup username. + + To update your own username (for users only, not bots) you can use :meth:`update_username`. + + Args: + chat_id (``int`` | ``str``) + Unique identifier (int) or username (str) of the target chat. + username (``str`` | ``None``): + Username to set. Pass "" (empty string) or None to remove the username. + + Returns: + True on success. + + Raises: + :class:`Error ` in case of a Telegram RPC error. + ``ValueError`` if a chat_id belongs to a user or chat. + """ + + peer = self.resolve_peer(chat_id) + + if isinstance(peer, types.InputPeerChannel): + return bool( + self.send( + functions.channels.UpdateUsername( + channel=peer, + username=username or "" + ) + ) + ) + else: + raise ValueError("The chat_id \"{}\" belongs to a user or chat".format(chat_id)) diff --git a/pyrogram/client/methods/messages/edit_message_media.py b/pyrogram/client/methods/messages/edit_message_media.py index 72dcf1b2..6c921968 100644 --- a/pyrogram/client/methods/messages/edit_message_media.py +++ b/pyrogram/client/methods/messages/edit_message_media.py @@ -17,7 +17,6 @@ # along with Pyrogram. If not, see . import binascii -import mimetypes import os import struct from typing import Union @@ -122,7 +121,8 @@ class EditMessageMedia(BaseClient): functions.messages.UploadMedia( peer=await self.resolve_peer(chat_id), media=types.InputMediaUploadedDocument( - mime_type=mimetypes.types_map[".mp4"], + mime_type="video/mp4", + thumb=None if media.thumb is None else self.save_file(media.thumb), file=await self.save_file(media.media), attributes=[ types.DocumentAttributeVideo( @@ -178,7 +178,8 @@ class EditMessageMedia(BaseClient): functions.messages.UploadMedia( peer=await self.resolve_peer(chat_id), media=types.InputMediaUploadedDocument( - mime_type=mimetypes.types_map.get("." + media.media.split(".")[-1], "audio/mpeg"), + mime_type="audio/mpeg", + thumb=None if media.thumb is None else self.save_file(media.thumb), file=await self.save_file(media.media), attributes=[ types.DocumentAttributeAudio( @@ -233,7 +234,8 @@ class EditMessageMedia(BaseClient): functions.messages.UploadMedia( peer=await self.resolve_peer(chat_id), media=types.InputMediaUploadedDocument( - mime_type=mimetypes.types_map[".mp4"], + mime_type="video/mp4", + thumb=None if media.thumb is None else self.save_file(media.thumb), file=await self.save_file(media.media), attributes=[ types.DocumentAttributeVideo( @@ -290,7 +292,8 @@ class EditMessageMedia(BaseClient): functions.messages.UploadMedia( peer=await self.resolve_peer(chat_id), media=types.InputMediaUploadedDocument( - mime_type=mimetypes.types_map.get("." + media.media.split(".")[-1], "text/plain"), + mime_type="application/zip", + thumb=None if media.thumb is None else self.save_file(media.thumb), file=await self.save_file(media.media), attributes=[ types.DocumentAttributeFilename(os.path.basename(media.media)) diff --git a/pyrogram/client/methods/messages/send_animation.py b/pyrogram/client/methods/messages/send_animation.py index 44d7d137..09db4bf0 100644 --- a/pyrogram/client/methods/messages/send_animation.py +++ b/pyrogram/client/methods/messages/send_animation.py @@ -17,7 +17,6 @@ # along with Pyrogram. If not, see . import binascii -import mimetypes import os import struct from typing import Union @@ -132,7 +131,7 @@ class SendAnimation(BaseClient): thumb = None if thumb is None else await self.save_file(thumb) file = await self.save_file(animation, progress=progress, progress_args=progress_args) media = types.InputMediaUploadedDocument( - mime_type=mimetypes.types_map[".mp4"], + mime_type="video/mp4", file=file, thumb=thumb, attributes=[ diff --git a/pyrogram/client/methods/messages/send_audio.py b/pyrogram/client/methods/messages/send_audio.py index 73208b25..3d67a222 100644 --- a/pyrogram/client/methods/messages/send_audio.py +++ b/pyrogram/client/methods/messages/send_audio.py @@ -17,7 +17,6 @@ # along with Pyrogram. If not, see . import binascii -import mimetypes import os import struct from typing import Union @@ -133,7 +132,7 @@ class SendAudio(BaseClient): thumb = None if thumb is None else await self.save_file(thumb) file = await self.save_file(audio, progress=progress, progress_args=progress_args) media = types.InputMediaUploadedDocument( - mime_type=mimetypes.types_map.get("." + audio.split(".")[-1], "audio/mpeg"), + mime_type="audio/mpeg", file=file, thumb=thumb, attributes=[ diff --git a/pyrogram/client/methods/messages/send_document.py b/pyrogram/client/methods/messages/send_document.py index e9ab9375..cef89c22 100644 --- a/pyrogram/client/methods/messages/send_document.py +++ b/pyrogram/client/methods/messages/send_document.py @@ -17,7 +17,6 @@ # along with Pyrogram. If not, see . import binascii -import mimetypes import os import struct from typing import Union @@ -119,7 +118,7 @@ class SendDocument(BaseClient): thumb = None if thumb is None else await self.save_file(thumb) file = await self.save_file(document, progress=progress, progress_args=progress_args) media = types.InputMediaUploadedDocument( - mime_type=mimetypes.types_map.get("." + document.split(".")[-1], "text/plain"), + mime_type="application/zip", file=file, thumb=thumb, attributes=[ diff --git a/pyrogram/client/methods/messages/send_media_group.py b/pyrogram/client/methods/messages/send_media_group.py index b4b28f4e..a6584a00 100644 --- a/pyrogram/client/methods/messages/send_media_group.py +++ b/pyrogram/client/methods/messages/send_media_group.py @@ -18,7 +18,6 @@ import binascii import logging -import mimetypes import os import struct from typing import Union, List @@ -130,7 +129,7 @@ class SendMediaGroup(BaseClient): media=types.InputMediaUploadedDocument( file=await self.save_file(i.media), thumb=None if i.thumb is None else self.save_file(i.thumb), - mime_type=mimetypes.types_map[".mp4"], + mime_type="video/mp4", attributes=[ types.DocumentAttributeVideo( supports_streaming=i.supports_streaming or None, diff --git a/pyrogram/client/methods/messages/send_video.py b/pyrogram/client/methods/messages/send_video.py index aecffe28..4d54f0f3 100644 --- a/pyrogram/client/methods/messages/send_video.py +++ b/pyrogram/client/methods/messages/send_video.py @@ -17,7 +17,6 @@ # along with Pyrogram. If not, see . import binascii -import mimetypes import os import struct from typing import Union @@ -136,7 +135,7 @@ class SendVideo(BaseClient): thumb = None if thumb is None else await self.save_file(thumb) file = await self.save_file(video, progress=progress, progress_args=progress_args) media = types.InputMediaUploadedDocument( - mime_type=mimetypes.types_map[".mp4"], + mime_type="video/mp4", file=file, thumb=thumb, attributes=[ diff --git a/pyrogram/client/methods/messages/send_video_note.py b/pyrogram/client/methods/messages/send_video_note.py index fbc6c984..c3a7c568 100644 --- a/pyrogram/client/methods/messages/send_video_note.py +++ b/pyrogram/client/methods/messages/send_video_note.py @@ -17,7 +17,6 @@ # along with Pyrogram. If not, see . import binascii -import mimetypes import os import struct from typing import Union @@ -116,7 +115,7 @@ class SendVideoNote(BaseClient): thumb = None if thumb is None else await self.save_file(thumb) file = await self.save_file(video_note, progress=progress, progress_args=progress_args) media = types.InputMediaUploadedDocument( - mime_type=mimetypes.types_map[".mp4"], + mime_type="video/mp4", file=file, thumb=thumb, attributes=[ diff --git a/pyrogram/client/methods/messages/send_voice.py b/pyrogram/client/methods/messages/send_voice.py index 8b6c8e61..35e8a7c6 100644 --- a/pyrogram/client/methods/messages/send_voice.py +++ b/pyrogram/client/methods/messages/send_voice.py @@ -17,7 +17,6 @@ # along with Pyrogram. If not, see . import binascii -import mimetypes import os import struct from typing import Union @@ -116,7 +115,7 @@ class SendVoice(BaseClient): if os.path.exists(voice): file = await self.save_file(voice, progress=progress, progress_args=progress_args) media = types.InputMediaUploadedDocument( - mime_type=mimetypes.types_map.get("." + voice.split(".")[-1], "audio/mpeg"), + mime_type="audio/mpeg", file=file, attributes=[ types.DocumentAttributeAudio( diff --git a/pyrogram/client/methods/users/__init__.py b/pyrogram/client/methods/users/__init__.py index db5e5869..f8c39650 100644 --- a/pyrogram/client/methods/users/__init__.py +++ b/pyrogram/client/methods/users/__init__.py @@ -21,6 +21,7 @@ from .get_me import GetMe from .get_user_profile_photos import GetUserProfilePhotos from .get_users import GetUsers from .set_user_profile_photo import SetUserProfilePhoto +from .update_username import UpdateUsername class Users( @@ -28,6 +29,7 @@ class Users( SetUserProfilePhoto, DeleteUserProfilePhotos, GetUsers, - GetMe + GetMe, + UpdateUsername ): pass diff --git a/pyrogram/client/methods/users/update_username.py b/pyrogram/client/methods/users/update_username.py new file mode 100644 index 00000000..9a4feb23 --- /dev/null +++ b/pyrogram/client/methods/users/update_username.py @@ -0,0 +1,51 @@ +# Pyrogram - Telegram MTProto API Client Library for Python +# Copyright (C) 2017-2019 Dan Tès +# +# This file is part of Pyrogram. +# +# Pyrogram is free software: you can redistribute it and/or modify +# it under the terms of the GNU Lesser General Public License as published +# by the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Pyrogram 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 General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public License +# along with Pyrogram. If not, see . + +from typing import Union + +from pyrogram.api import functions +from ...ext import BaseClient + + +class UpdateUsername(BaseClient): + def update_username(self, + username: Union[str, None]) -> bool: + """Use this method to update your own username. + + This method only works for users, not bots. Bot usernames must be changed via Bot Support or by recreating + them from scratch using BotFather. To update a channel or supergroup username you can use + :meth:`update_chat_username`. + + Args: + username (``str`` | ``None``): + Username to set. "" (empty string) or None to remove the username. + + Returns: + True on success. + + Raises: + :class:`Error ` in case of a Telegram RPC error. + """ + + return bool( + self.send( + functions.account.UpdateUsername( + username=username or "" + ) + ) + ) diff --git a/pyrogram/client/types/bots/inline_keyboard_button.py b/pyrogram/client/types/bots/inline_keyboard_button.py index cd30f373..cc829cb1 100644 --- a/pyrogram/client/types/bots/inline_keyboard_button.py +++ b/pyrogram/client/types/bots/inline_keyboard_button.py @@ -63,7 +63,7 @@ class InlineKeyboardButton(PyrogramType): callback_game: CallbackGame = None): super().__init__(None) - self.text = text + self.text = str(text) self.url = url self.callback_data = callback_data self.switch_inline_query = switch_inline_query diff --git a/pyrogram/client/types/bots/keyboard_button.py b/pyrogram/client/types/bots/keyboard_button.py index e93eccb3..3c7c2bd6 100644 --- a/pyrogram/client/types/bots/keyboard_button.py +++ b/pyrogram/client/types/bots/keyboard_button.py @@ -46,7 +46,7 @@ class KeyboardButton(PyrogramType): request_location: bool = None): super().__init__(None) - self.text = text + self.text = str(text) self.request_contact = request_contact self.request_location = request_location diff --git a/pyrogram/client/types/user_and_chats/chat.py b/pyrogram/client/types/user_and_chats/chat.py index de1cd633..300ecc68 100644 --- a/pyrogram/client/types/user_and_chats/chat.py +++ b/pyrogram/client/types/user_and_chats/chat.py @@ -209,3 +209,14 @@ class Chat(PyrogramType): parsed_chat.invite_link = full_chat.exported_invite.link return parsed_chat + + @staticmethod + def _parse_chat(client, chat): + # A wrapper around each entity parser: User, Chat and Channel. + # Currently unused, might become useful in future. + if isinstance(chat, types.Chat): + return Chat._parse_chat_chat(client, chat) + elif isinstance(chat, types.User): + return Chat._parse_user_chat(client, chat) + else: + return Chat._parse_channel_chat(client, chat)