2
0
mirror of https://github.com/pyrogram/pyrogram synced 2025-09-02 23:35:17 +00:00

Merge branch 'develop' into layer-95

# Conflicts:
#	pyrogram/__init__.py
This commit is contained in:
Dan
2019-03-01 18:38:27 +01:00
48 changed files with 3440 additions and 279 deletions

View File

@@ -18,12 +18,18 @@
import sys
if sys.version_info[:3] in [(3, 5, 0), (3, 5, 1), (3, 5, 2)]:
from .vendor import typing
# Monkey patch the standard "typing" module because Python versions from 3.5.0 to 3.5.2 have a broken one.
sys.modules["typing"] = typing
__copyright__ = "Copyright (C) 2017-2019 Dan Tès <https://github.com/delivrance>".replace(
"\xe8",
"e" if sys.getfilesystemencoding() != "utf-8" else "\xe8"
)
__license__ = "GNU Lesser General Public License v3 or later (LGPLv3+)"
__version__ = "0.10.4.develop"
__version__ = "0.11.1.develop"
from .api.errors import Error
from .client.types import (
@@ -32,8 +38,8 @@ from .client.types import (
Location, Message, MessageEntity, Dialog, Dialogs, Photo, PhotoSize, Sticker, User, UserStatus,
UserProfilePhotos, Venue, Animation, Video, VideoNote, Voice, CallbackQuery, Messages, ForceReply,
InlineKeyboardButton, InlineKeyboardMarkup, KeyboardButton, ReplyKeyboardMarkup, ReplyKeyboardRemove,
Poll, PollOption, ChatPreview, StopPropagation, Game, CallbackGame, GameHighScore, GameHighScores,
ChatPermissions
Poll, PollOption, ChatPreview, StopPropagation, ContinuePropagation, Game, CallbackGame, GameHighScore,
GameHighScores, ChatPermissions
)
from .client import (
Client, ChatAction, ParseMode, Emoji,

View File

@@ -29,6 +29,7 @@ import struct
import tempfile
import threading
import time
import warnings
from configparser import ConfigParser
from datetime import datetime
from hashlib import sha256, md5
@@ -67,10 +68,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
@@ -139,8 +140,13 @@ class Client(Methods, BaseClient):
Only applicable for new sessions.
first_name (``str``, *optional*):
Pass a First Name to avoid entering it manually. It will be used to automatically
create a new Telegram account in case the phone number you passed is not registered yet.
Pass a First Name as string to avoid entering it manually. Or pass a callback function which accepts no
arguments and must return the correct name as string (e.g., "Dan"). It will be used to automatically create
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*):
@@ -157,10 +163,9 @@ class Client(Methods, BaseClient):
config_file (``str``, *optional*):
Path of the configuration file. Defaults to ./config.ini
plugins_dir (``str``, *optional*):
Define a custom directory for your plugins. The plugins directory is the location in your
filesystem where Pyrogram will automatically load your update handlers.
Defaults to None (plugins disabled).
plugins (``dict``, *optional*):
Your Smart Plugins settings as dict, e.g.: *dict(root="plugins")*.
This is an alternative way to setup plugins if you don't want to use the *config.ini* file.
no_updates (``bool``, *optional*):
Pass True to completely disable incoming updates for the current session.
@@ -192,12 +197,13 @@ 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,
workdir: str = BaseClient.WORKDIR,
config_file: str = BaseClient.CONFIG_FILE,
plugins_dir: str = None,
plugins: dict = None,
no_updates: bool = None,
takeout: bool = None):
super().__init__()
@@ -218,12 +224,13 @@ 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
self.workdir = workdir
self.config_file = config_file
self.plugins_dir = plugins_dir
self.plugins = plugins
self.no_updates = no_updates
self.takeout = takeout
@@ -263,8 +270,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()
self.load_session()
@@ -282,13 +294,15 @@ class Client(Methods, BaseClient):
try:
if self.user_id is None:
if self.bot_token is None:
self.is_bot = False
self.authorize_user()
else:
self.is_bot = True
self.authorize_bot()
self.save_session()
if self.bot_token is None:
if not self.is_bot:
if self.takeout:
self.takeout_id = self.send(functions.account.InitTakeoutSession()).id
log.warning("Takeout session {} initiated".format(self.takeout_id))
@@ -1075,6 +1089,31 @@ class Client(Methods, BaseClient):
self._proxy["username"] = parser.get("proxy", "username", fallback=None) or None
self._proxy["password"] = parser.get("proxy", "password", fallback=None) or None
if self.plugins:
self.plugins["enabled"] = bool(self.plugins.get("enabled", True))
self.plugins["include"] = "\n".join(self.plugins.get("include", [])) or None
self.plugins["exclude"] = "\n".join(self.plugins.get("exclude", [])) or None
else:
try:
section = parser["plugins"]
self.plugins = {
"enabled": section.getboolean("enabled", True),
"root": section.get("root"),
"include": section.get("include") or None,
"exclude": section.get("exclude") or None
}
except KeyError:
self.plugins = {}
if self.plugins:
for option in ["include", "exclude"]:
if self.plugins[option] is not None:
self.plugins[option] = [
(i.split()[0], i.split()[1:] or None)
for i in self.plugins[option].strip().split("\n")
]
def load_session(self):
try:
with open(os.path.join(self.workdir, "{}.session".format(self.session_name)), encoding="utf-8") as f:
@@ -1089,6 +1128,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)
@@ -1106,43 +1147,108 @@ class Client(Methods, BaseClient):
self.peers_by_phone[k] = peer
def load_plugins(self):
if self.plugins_dir is not None:
plugins_count = 0
if self.plugins.get("enabled", False):
root = self.plugins["root"]
include = self.plugins["include"]
exclude = self.plugins["exclude"]
for path in Path(self.plugins_dir).rglob("*.py"):
file_path = os.path.splitext(str(path))[0]
import_path = []
count = 0
while file_path:
file_path, tail = os.path.split(file_path)
import_path.insert(0, tail)
if include is None:
for path in sorted(Path(root).rglob("*.py")):
module_path = '.'.join(path.parent.parts + (path.stem,))
module = import_module(module_path)
import_path = ".".join(import_path)
module = import_module(import_path)
for name in vars(module).keys():
# noinspection PyBroadException
try:
handler, group = getattr(module, name)
for name in dir(module):
# noinspection PyBroadException
try:
handler, group = getattr(module, name)
if isinstance(handler, Handler) and isinstance(group, int):
self.add_handler(handler, group)
if isinstance(handler, Handler) and isinstance(group, int):
self.add_handler(handler, group)
log.info('[LOAD] {}("{}") in group {} from "{}"'.format(
type(handler).__name__, name, group, module_path))
log.info('{}("{}") from "{}" loaded in group {}'.format(
type(handler).__name__, name, import_path, group))
plugins_count += 1
except Exception:
pass
if plugins_count > 0:
log.warning('Successfully loaded {} plugin{} from "{}"'.format(
plugins_count,
"s" if plugins_count > 1 else "",
self.plugins_dir
))
count += 1
except Exception:
pass
else:
log.warning('No plugin loaded: "{}" doesn\'t contain any valid plugin'.format(self.plugins_dir))
for path, handlers in include:
module_path = root + "." + path
warn_non_existent_functions = True
try:
module = import_module(module_path)
except ModuleNotFoundError:
log.warning('[LOAD] Ignoring non-existent module "{}"'.format(module_path))
continue
if "__path__" in dir(module):
log.warning('[LOAD] Ignoring namespace "{}"'.format(module_path))
continue
if handlers is None:
handlers = vars(module).keys()
warn_non_existent_functions = False
for name in handlers:
# noinspection PyBroadException
try:
handler, group = getattr(module, name)
if isinstance(handler, Handler) and isinstance(group, int):
self.add_handler(handler, group)
log.info('[LOAD] {}("{}") in group {} from "{}"'.format(
type(handler).__name__, name, group, module_path))
count += 1
except Exception:
if warn_non_existent_functions:
log.warning('[LOAD] Ignoring non-existent function "{}" from "{}"'.format(
name, module_path))
if exclude is not None:
for path, handlers in exclude:
module_path = root + "." + path
warn_non_existent_functions = True
try:
module = import_module(module_path)
except ModuleNotFoundError:
log.warning('[UNLOAD] Ignoring non-existent module "{}"'.format(module_path))
continue
if "__path__" in dir(module):
log.warning('[UNLOAD] Ignoring namespace "{}"'.format(module_path))
continue
if handlers is None:
handlers = vars(module).keys()
warn_non_existent_functions = False
for name in handlers:
# noinspection PyBroadException
try:
handler, group = getattr(module, name)
if isinstance(handler, Handler) and isinstance(group, int):
self.remove_handler(handler, group)
log.info('[UNLOAD] {}("{}") from group {} in "{}"'.format(
type(handler).__name__, name, group, module_path))
count -= 1
except Exception:
if warn_non_existent_functions:
log.warning('[UNLOAD] Ignoring non-existent function "{}" from "{}"'.format(
name, module_path))
if count > 0:
log.warning('Successfully loaded {} plugin{} from "{}"'.format(count, "s" if count > 1 else "", root))
else:
log.warning('No plugin loaded from "{}"'.format(root))
def save_session(self):
auth_key = base64.b64encode(self.auth_key).decode()
@@ -1157,7 +1263,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
@@ -1350,21 +1457,25 @@ class Client(Methods, BaseClient):
md5_sum = "".join([hex(i)[2:].zfill(2) for i in md5_sum.digest()])
break
if is_big:
rpc = functions.upload.SaveBigFilePart(
file_id=file_id,
file_part=file_part,
file_total_parts=file_total_parts,
bytes=chunk
)
else:
rpc = functions.upload.SaveFilePart(
file_id=file_id,
file_part=file_part,
bytes=chunk
)
for _ in range(3):
if is_big:
rpc = functions.upload.SaveBigFilePart(
file_id=file_id,
file_part=file_part,
file_total_parts=file_total_parts,
bytes=chunk
)
else:
rpc = functions.upload.SaveFilePart(
file_id=file_id,
file_part=file_part,
bytes=chunk
)
assert session.send(rpc), "Couldn't upload file"
if session.send(rpc):
break
else:
raise AssertionError("Telegram didn't accept chunk #{} of {}".format(file_part, path))
if is_missing_part:
return

View File

@@ -128,35 +128,37 @@ class Dispatcher:
parser = self.update_parsers.get(type(update), None)
if parser is None:
continue
parsed_update, handler_type = parser(update, users, chats)
parsed_update, handler_type = (
parser(update, users, chats)
if parser is not None
else (None, type(None))
)
for group in self.groups.values():
try:
for handler in group:
args = None
for handler in group:
args = None
if isinstance(handler, RawUpdateHandler):
args = (update, users, chats)
elif isinstance(handler, handler_type):
if handler.check(parsed_update):
args = (parsed_update,)
if isinstance(handler, handler_type):
if handler.check(parsed_update):
args = (parsed_update,)
elif isinstance(handler, RawUpdateHandler):
args = (update, users, chats)
if args is None:
continue
if args is None:
continue
try:
handler.callback(self.client, *args)
except StopIteration:
raise
except Exception as e:
log.error(e, exc_info=True)
try:
handler.callback(self.client, *args)
except pyrogram.StopPropagation:
raise
except pyrogram.ContinuePropagation:
continue
except Exception as e:
log.error(e, exc_info=True)
break
except StopIteration:
break
except pyrogram.StopPropagation:
pass
except Exception as e:
log.error(e, exc_info=True)

View File

@@ -68,7 +68,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

View File

@@ -94,6 +94,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()

View File

@@ -62,16 +62,16 @@ class Filters:
create = create
me = create("Me", lambda _, m: bool(m.from_user and m.from_user.is_self))
"""Filter messages coming from you yourself"""
"""Filter messages generated by you yourself."""
bot = create("Bot", lambda _, m: bool(m.from_user and m.from_user.is_bot))
"""Filter messages coming from bots"""
"""Filter messages coming from bots."""
incoming = create("Incoming", lambda _, m: not m.outgoing)
"""Filter incoming messages."""
"""Filter incoming messages. Messages sent to your own chat (Saved Messages) are also recognised as incoming."""
outgoing = create("Outgoing", lambda _, m: m.outgoing)
"""Filter outgoing messages."""
"""Filter outgoing messages. Messages sent to your own chat (Saved Messages) are not recognized as outgoing."""
text = create("Text", lambda _, m: bool(m.text))
"""Filter text messages."""

View File

@@ -45,10 +45,3 @@ class CallbackQueryHandler(Handler):
def __init__(self, callback: callable, filters=None):
super().__init__(callback, filters)
def check(self, callback_query):
return (
self.filters(callback_query)
if callable(self.filters)
else True
)

View File

@@ -48,8 +48,4 @@ class DeletedMessagesHandler(Handler):
super().__init__(callback, filters)
def check(self, messages):
return (
self.filters(messages.messages[0])
if callable(self.filters)
else True
)
return super().check(messages.messages[0])

View File

@@ -21,3 +21,10 @@ class Handler:
def __init__(self, callback: callable, filters=None):
self.callback = callback
self.filters = filters
def check(self, update):
return (
self.filters(update)
if callable(self.filters)
else True
)

View File

@@ -46,10 +46,3 @@ class MessageHandler(Handler):
def __init__(self, callback: callable, filters=None):
super().__init__(callback, filters)
def check(self, message):
return (
self.filters(message)
if callable(self.filters)
else True
)

View File

@@ -45,10 +45,3 @@ class UserStatusHandler(Handler):
def __init__(self, callback: callable, filters=None):
super().__init__(callback, filters)
def check(self, user_status):
return (
self.filters(user_status)
if callable(self.filters)
else True
)

View File

@@ -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

View File

@@ -16,6 +16,7 @@
# You should have received a copy of the GNU Lesser General Public License
# along with Pyrogram. If not, see <http://www.gnu.org/licenses/>.
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 <pyrogram.Chat>` object is returned.
Raises:
:class:`Error <pyrogram.Error>` in case of a Telegram RPC error.
"""
match = self.INVITE_LINK_RE.match(chat_id)
if match:
return self.send(
chat = 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 = self.send(
functions.contacts.ResolveUsername(
@@ -53,8 +61,10 @@ class JoinChat(BaseClient):
access_hash=resolved_peer.chats[0].access_hash
)
return self.send(
chat = self.send(
functions.channels.JoinChannel(
channel=channel
)
)
return pyrogram.Chat._parse_channel_chat(self, chat.chats[0])

View File

@@ -0,0 +1,59 @@
# Pyrogram - Telegram MTProto API Client Library for Python
# Copyright (C) 2017-2019 Dan Tès <https://github.com/delivrance>
#
# 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 <http://www.gnu.org/licenses/>.
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 <pyrogram.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))

View File

@@ -19,7 +19,8 @@
import logging
import time
from pyrogram.api import functions, types
import pyrogram
from pyrogram.api import functions
from pyrogram.api.errors import FloodWait
from ...ext import BaseClient
@@ -28,12 +29,10 @@ log = logging.getLogger(__name__)
class GetContacts(BaseClient):
def get_contacts(self):
"""Use this method to get contacts from your Telegram address book
Requires no parameters.
"""Use this method to get contacts from your Telegram address book.
Returns:
On success, the user's contacts are returned
On success, a list of :obj:`User` objects is returned.
Raises:
:class:`Error <pyrogram.Error>` in case of a Telegram RPC error.
@@ -44,9 +43,6 @@ class GetContacts(BaseClient):
except FloodWait as e:
log.warning("get_contacts flood: waiting {} seconds".format(e.x))
time.sleep(e.x)
continue
else:
if isinstance(contacts, types.contacts.Contacts):
log.info("Total contacts: {}".format(len(self.peers_by_phone)))
return contacts
log.info("Total contacts: {}".format(len(self.peers_by_phone)))
return [pyrogram.User._parse(self, user) for user in contacts.users]

View File

@@ -17,7 +17,6 @@
# along with Pyrogram. If not, see <http://www.gnu.org/licenses/>.
import binascii
import mimetypes
import os
import struct
from typing import Union
@@ -122,7 +121,8 @@ class EditMessageMedia(BaseClient):
functions.messages.UploadMedia(
peer=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=self.save_file(media.media),
attributes=[
types.DocumentAttributeVideo(
@@ -178,7 +178,8 @@ class EditMessageMedia(BaseClient):
functions.messages.UploadMedia(
peer=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=self.save_file(media.media),
attributes=[
types.DocumentAttributeAudio(
@@ -233,7 +234,8 @@ class EditMessageMedia(BaseClient):
functions.messages.UploadMedia(
peer=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=self.save_file(media.media),
attributes=[
types.DocumentAttributeVideo(
@@ -290,7 +292,8 @@ class EditMessageMedia(BaseClient):
functions.messages.UploadMedia(
peer=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=self.save_file(media.media),
attributes=[
types.DocumentAttributeFilename(os.path.basename(media.media))

View File

@@ -16,12 +16,17 @@
# You should have received a copy of the GNU Lesser General Public License
# along with Pyrogram. If not, see <http://www.gnu.org/licenses/>.
import logging
import time
from typing import Union
import pyrogram
from pyrogram.api import functions
from pyrogram.api.errors import FloodWait
from ...ext import BaseClient
log = logging.getLogger(__name__)
class GetHistory(BaseClient):
def get_history(self,
@@ -66,21 +71,28 @@ class GetHistory(BaseClient):
:class:`Error <pyrogram.Error>` in case of a Telegram RPC error.
"""
messages = pyrogram.Messages._parse(
self,
self.send(
functions.messages.GetHistory(
peer=self.resolve_peer(chat_id),
offset_id=offset_id,
offset_date=offset_date,
add_offset=offset * (-1 if reverse else 1) - (limit if reverse else 0),
limit=limit,
max_id=0,
min_id=0,
hash=0
while True:
try:
messages = pyrogram.Messages._parse(
self,
self.send(
functions.messages.GetHistory(
peer=self.resolve_peer(chat_id),
offset_id=offset_id,
offset_date=offset_date,
add_offset=offset * (-1 if reverse else 1) - (limit if reverse else 0),
limit=limit,
max_id=0,
min_id=0,
hash=0
)
)
)
)
)
except FloodWait as e:
log.warning("Sleeping for {}s".format(e.x))
time.sleep(e.x)
else:
break
if reverse:
messages.messages.reverse()

View File

@@ -17,7 +17,6 @@
# along with Pyrogram. If not, see <http://www.gnu.org/licenses/>.
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 self.save_file(thumb)
file = 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=[

View File

@@ -17,7 +17,6 @@
# along with Pyrogram. If not, see <http://www.gnu.org/licenses/>.
import binascii
import mimetypes
import os
import struct
from typing import Union
@@ -134,7 +133,7 @@ class SendAudio(BaseClient):
thumb = None if thumb is None else self.save_file(thumb)
file = 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=[

View File

@@ -17,7 +17,6 @@
# along with Pyrogram. If not, see <http://www.gnu.org/licenses/>.
import binascii
import mimetypes
import os
import struct
from typing import Union
@@ -120,7 +119,7 @@ class SendDocument(BaseClient):
thumb = None if thumb is None else self.save_file(thumb)
file = 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=[

View File

@@ -17,20 +17,22 @@
# along with Pyrogram. If not, see <http://www.gnu.org/licenses/>.
import binascii
import mimetypes
import logging
import os
import struct
import time
from typing import Union, List
import pyrogram
from pyrogram.api import functions, types
from pyrogram.api.errors import FileIdInvalid
from pyrogram.api.errors import FileIdInvalid, FloodWait
from pyrogram.client.ext import BaseClient, utils
log = logging.getLogger(__name__)
class SendMediaGroup(BaseClient):
# TODO: Add progress parameter
# TODO: Return new Message object
# TODO: Figure out how to send albums using URLs
def send_media_group(self,
chat_id: Union[int, str],
@@ -38,7 +40,6 @@ class SendMediaGroup(BaseClient):
disable_notification: bool = None,
reply_to_message_id: int = None):
"""Use this method to send a group of photos or videos as an album.
On success, an Update containing the sent Messages is returned.
Args:
chat_id (``int`` | ``str``):
@@ -57,6 +58,13 @@ class SendMediaGroup(BaseClient):
reply_to_message_id (``int``, *optional*):
If the message is a reply, ID of the original message.
Returns:
On success, a :obj:`Messages <pyrogram.Messages>` object is returned containing all the
single messages sent.
Raises:
:class:`Error <pyrogram.Error>` in case of a Telegram RPC error.
"""
multi_media = []
@@ -65,14 +73,21 @@ class SendMediaGroup(BaseClient):
if isinstance(i, pyrogram.InputMediaPhoto):
if os.path.exists(i.media):
media = self.send(
functions.messages.UploadMedia(
peer=self.resolve_peer(chat_id),
media=types.InputMediaUploadedPhoto(
file=self.save_file(i.media)
while True:
try:
media = self.send(
functions.messages.UploadMedia(
peer=self.resolve_peer(chat_id),
media=types.InputMediaUploadedPhoto(
file=self.save_file(i.media)
)
)
)
)
)
except FloodWait as e:
log.warning("Sleeping for {}s".format(e.x))
time.sleep(e.x)
else:
break
media = types.InputMediaPhoto(
id=types.InputPhoto(
@@ -106,25 +121,32 @@ class SendMediaGroup(BaseClient):
)
elif isinstance(i, pyrogram.InputMediaVideo):
if os.path.exists(i.media):
media = self.send(
functions.messages.UploadMedia(
peer=self.resolve_peer(chat_id),
media=types.InputMediaUploadedDocument(
file=self.save_file(i.media),
thumb=None if i.thumb is None else self.save_file(i.thumb),
mime_type=mimetypes.types_map[".mp4"],
attributes=[
types.DocumentAttributeVideo(
supports_streaming=i.supports_streaming or None,
duration=i.duration,
w=i.width,
h=i.height
),
types.DocumentAttributeFilename(os.path.basename(i.media))
]
while True:
try:
media = self.send(
functions.messages.UploadMedia(
peer=self.resolve_peer(chat_id),
media=types.InputMediaUploadedDocument(
file=self.save_file(i.media),
thumb=None if i.thumb is None else self.save_file(i.thumb),
mime_type="video/mp4",
attributes=[
types.DocumentAttributeVideo(
supports_streaming=i.supports_streaming or None,
duration=i.duration,
w=i.width,
h=i.height
),
types.DocumentAttributeFilename(os.path.basename(i.media))
]
)
)
)
)
)
except FloodWait as e:
log.warning("Sleeping for {}s".format(e.x))
time.sleep(e.x)
else:
break
media = types.InputMediaDocument(
id=types.InputDocument(
@@ -165,11 +187,30 @@ class SendMediaGroup(BaseClient):
)
)
return self.send(
functions.messages.SendMultiMedia(
peer=self.resolve_peer(chat_id),
multi_media=multi_media,
silent=disable_notification or None,
reply_to_msg_id=reply_to_message_id
while True:
try:
r = self.send(
functions.messages.SendMultiMedia(
peer=self.resolve_peer(chat_id),
multi_media=multi_media,
silent=disable_notification or None,
reply_to_msg_id=reply_to_message_id
)
)
except FloodWait as e:
log.warning("Sleeping for {}s".format(e.x))
time.sleep(e.x)
else:
break
return pyrogram.Messages._parse(
self,
types.messages.Messages(
messages=[m.message for m in filter(
lambda u: isinstance(u, (types.UpdateNewMessage, types.UpdateNewChannelMessage)),
r.updates
)],
users=r.users,
chats=r.chats
)
)

View File

@@ -17,7 +17,6 @@
# along with Pyrogram. If not, see <http://www.gnu.org/licenses/>.
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 self.save_file(thumb)
file = 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=[

View File

@@ -17,7 +17,6 @@
# along with Pyrogram. If not, see <http://www.gnu.org/licenses/>.
import binascii
import mimetypes
import os
import struct
from typing import Union
@@ -117,7 +116,7 @@ class SendVideoNote(BaseClient):
thumb = None if thumb is None else self.save_file(thumb)
file = 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=[

View File

@@ -17,7 +17,6 @@
# along with Pyrogram. If not, see <http://www.gnu.org/licenses/>.
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 = 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(

View File

@@ -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

View File

@@ -0,0 +1,51 @@
# Pyrogram - Telegram MTProto API Client Library for Python
# Copyright (C) 2017-2019 Dan Tès <https://github.com/delivrance>
#
# 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 <http://www.gnu.org/licenses/>.
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 <pyrogram.Error>` in case of a Telegram RPC error.
"""
return bool(
self.send(
functions.account.UpdateUsername(
username=username or ""
)
)
)

View File

@@ -30,7 +30,7 @@ from .messages_and_media import (
Sticker, Venue, Video, VideoNote, Voice, UserProfilePhotos,
Message, Messages, MessageEntity, Poll, PollOption, Game
)
from .update import StopPropagation
from .update import StopPropagation, ContinuePropagation
from .user_and_chats import (
Chat, ChatMember, ChatMembers, ChatPhoto,
Dialog, Dialogs, User, UserStatus, ChatPreview, ChatPermissions

View File

@@ -40,15 +40,15 @@ class CallbackQuery(PyrogramType, Update):
Sender.
chat_instance (``str``, *optional*):
Global identifier, uniquely corresponding to the chat to which the message with the callback button was
sent. Useful for high scores in games.
message (:obj:`Message <pyrogram.Message>`, *optional*):
Message with the callback button that originated the query. Note that message content and message date will
not be available if the message is too old.
message (:obj:`Message <pyrogram.Message>`, *optional*):
Identifier of the message sent via the bot in inline mode, that originated the query.
inline_message_id (``str``):
Global identifier, uniquely corresponding to the chat to which the message with the callback button was
sent. Useful for high scores in games.
Identifier of the message sent via the bot in inline mode, that originated the query.
data (``bytes``, *optional*):
Data associated with the callback button. Be aware that a bad client can send arbitrary data in this field.
@@ -72,9 +72,9 @@ class CallbackQuery(PyrogramType, Update):
self.id = id
self.from_user = from_user
self.chat_instance = chat_instance
self.message = message
self.inline_message_id = inline_message_id
self.chat_instance = chat_instance
self.data = data
self.game_short_name = game_short_name

View File

@@ -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

View File

@@ -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

View File

@@ -21,6 +21,13 @@ class StopPropagation(StopIteration):
pass
class ContinuePropagation(StopIteration):
pass
class Update:
def stop_propagation(self):
raise StopPropagation
def continue_propagation(self):
raise ContinuePropagation

View File

@@ -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)

19
pyrogram/vendor/__init__.py vendored Normal file
View File

@@ -0,0 +1,19 @@
# Pyrogram - Telegram MTProto API Client Library for Python
# Copyright (C) 2017-2019 Dan Tès <https://github.com/delivrance>
#
# 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 <http://www.gnu.org/licenses/>.
from .typing import typing

17
pyrogram/vendor/typing/__init__.py vendored Normal file
View File

@@ -0,0 +1,17 @@
# Pyrogram - Telegram MTProto API Client Library for Python
# Copyright (C) 2017-2019 Dan Tès <https://github.com/delivrance>
#
# 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 <http://www.gnu.org/licenses/>.

2413
pyrogram/vendor/typing/typing.py vendored Normal file

File diff suppressed because it is too large Load Diff