Merge branch 'develop' into asyncio

# Conflicts:
#	pyrogram/__init__.py
#	pyrogram/client/client.py
#	pyrogram/client/ext/base_client.py
#	pyrogram/client/methods/chats/get_chat_members.py
#	pyrogram/client/methods/chats/get_dialogs.py
#	pyrogram/client/methods/chats/kick_chat_member.py
#	pyrogram/client/methods/messages/get_history.py
#	pyrogram/client/methods/messages/get_messages.py
#	pyrogram/client/types/messages_and_media/messages.py
This commit is contained in:
Dan 2019-01-07 08:46:28 +01:00
commit 2084a406a4
31 changed files with 741 additions and 94 deletions

View File

@ -61,7 +61,7 @@ and documentation. Any help is appreciated!
Copyright & License Copyright & License
------------------- -------------------
- Copyright (C) 2017-2018 Dan Tès <https://github.com/delivrance> - Copyright (C) 2017-2019 Dan Tès <https://github.com/delivrance>
- Licensed under the terms of the `GNU Lesser General Public License v3 or later (LGPLv3+)`_ - Licensed under the terms of the `GNU Lesser General Public License v3 or later (LGPLv3+)`_
.. _`Telegram`: https://telegram.org/ .. _`Telegram`: https://telegram.org/

View File

@ -26,7 +26,7 @@ NOTICE_PATH = "NOTICE"
SECTION_RE = re.compile(r"---(\w+)---") SECTION_RE = re.compile(r"---(\w+)---")
LAYER_RE = re.compile(r"//\sLAYER\s(\d+)") LAYER_RE = re.compile(r"//\sLAYER\s(\d+)")
COMBINATOR_RE = re.compile(r"^([\w.]+)#([0-9a-f]+)\s(?:.*)=\s([\w<>.]+);(?: // Docs: (.+))?$", re.MULTILINE) COMBINATOR_RE = re.compile(r"^([\w.]+)#([0-9a-f]+)\s(?:.*)=\s([\w<>.]+);(?: // Docs: (.+))?$", re.MULTILINE)
ARGS_RE = re.compile("[^{](\w+):([\w?!.<>]+)") ARGS_RE = re.compile("[^{](\w+):([\w?!.<>#]+)")
FLAGS_RE = re.compile(r"flags\.(\d+)\?") FLAGS_RE = re.compile(r"flags\.(\d+)\?")
FLAGS_RE_2 = re.compile(r"flags\.(\d+)\?([\w<>.]+)") FLAGS_RE_2 = re.compile(r"flags\.(\d+)\?([\w<>.]+)")
FLAGS_RE_3 = re.compile(r"flags:#") FLAGS_RE_3 = re.compile(r"flags:#")
@ -288,17 +288,20 @@ def start():
sorted_args = sort_args(c.args) sorted_args = sort_args(c.args)
arguments = ", " + ", ".join( arguments = ", " + ", ".join(
[get_argument_type(i) for i in sorted_args] [get_argument_type(i) for i in sorted_args if i != ("flags", "#")]
) if c.args else "" ) if c.args else ""
fields = "\n ".join( fields = "\n ".join(
["self.{0} = {0} # {1}".format(i[0], i[1]) for i in c.args] ["self.{0} = {0} # {1}".format(i[0], i[1]) for i in c.args if i != ("flags", "#")]
) if c.args else "pass" ) if c.args else "pass"
docstring_args = [] docstring_args = []
docs = c.docs.split("|")[1:] if c.docs else None docs = c.docs.split("|")[1:] if c.docs else None
for i, arg in enumerate(sorted_args): for i, arg in enumerate(sorted_args):
if arg == ("flags", "#"):
continue
arg_name, arg_type = arg arg_name, arg_type = arg
is_optional = FLAGS_RE.match(arg_type) is_optional = FLAGS_RE.match(arg_type)
flag_number = is_optional.group(1) if is_optional else -1 flag_number = is_optional.group(1) if is_optional else -1
@ -338,27 +341,30 @@ def start():
if references: if references:
docstring_args += "\n\n See Also:\n This object can be returned by " + references + "." docstring_args += "\n\n See Also:\n This object can be returned by " + references + "."
if c.has_flags: write_types = read_types = "" if c.has_flags else "# No flags\n "
for arg_name, arg_type in c.args:
flag = FLAGS_RE_2.findall(arg_type)
if arg_name == "flags" and arg_type == "#":
write_flags = [] write_flags = []
for i in c.args: for i in c.args:
flag = FLAGS_RE.match(i[1]) flag = FLAGS_RE.match(i[1])
if flag: if flag:
write_flags.append("flags |= (1 << {}) if self.{} is not None else 0".format(flag.group(1), i[0])) write_flags.append(
"flags |= (1 << {}) if self.{} is not None else 0".format(flag.group(1), i[0]))
write_flags = "\n ".join([ write_flags = "\n ".join([
"flags = 0", "flags = 0",
"\n ".join(write_flags), "\n ".join(write_flags),
"b.write(Int(flags))" "b.write(Int(flags))\n "
]) ])
else:
write_flags = "# No flags"
read_flags = "flags = Int.read(b)" if c.has_flags else "# No flags" write_types += write_flags
read_types += "flags = Int.read(b)\n "
write_types = read_types = "" continue
for arg_name, arg_type in c.args:
flag = FLAGS_RE_2.findall(arg_type)
if flag: if flag:
index, flag_type = flag[0] index, flag_type = flag[0]
@ -448,11 +454,9 @@ def start():
object_id=c.id, object_id=c.id,
arguments=arguments, arguments=arguments,
fields=fields, fields=fields,
read_flags=read_flags,
read_types=read_types, read_types=read_types,
write_flags=write_flags,
write_types=write_types, write_types=write_types,
return_arguments=", ".join([i[0] for i in sorted_args]) return_arguments=", ".join([i[0] for i in sorted_args if i != ("flags", "#")])
) )
) )

View File

@ -16,7 +16,6 @@ class {class_name}(Object):
@staticmethod @staticmethod
def read(b: BytesIO, *args) -> "{class_name}": def read(b: BytesIO, *args) -> "{class_name}":
{read_flags}
{read_types} {read_types}
return {class_name}({return_arguments}) return {class_name}({return_arguments})
@ -24,6 +23,5 @@ class {class_name}(Object):
b = BytesIO() b = BytesIO()
b.write(Int(self.ID, False)) b.write(Int(self.ID, False))
{write_flags}
{write_types} {write_types}
return b.getvalue() return b.getvalue()

View File

@ -81,3 +81,7 @@ INPUT_USER_DEACTIVATED The target user has been deactivated
PASSWORD_RECOVERY_NA The password recovery e-mail is not available PASSWORD_RECOVERY_NA The password recovery e-mail is not available
PASSWORD_EMPTY The password entered is empty PASSWORD_EMPTY The password entered is empty
PHONE_NUMBER_FLOOD This number has tried to login too many times PHONE_NUMBER_FLOOD This number has tried to login too many times
TAKEOUT_INVALID The takeout id is invalid
TAKEOUT_REQUIRED The method must be invoked inside a takeout session
MESSAGE_POLL_CLOSED You can't interact with a closed poll
MEDIA_INVALID The media is invalid
1 id message
81 PASSWORD_RECOVERY_NA The password recovery e-mail is not available
82 PASSWORD_EMPTY The password entered is empty
83 PHONE_NUMBER_FLOOD This number has tried to login too many times
84 TAKEOUT_INVALID The takeout id is invalid
85 TAKEOUT_REQUIRED The method must be invoked inside a takeout session
86 MESSAGE_POLL_CLOSED You can't interact with a closed poll
87 MEDIA_INVALID The media is invalid

View File

@ -62,6 +62,7 @@ Messages
delete_messages delete_messages
get_messages get_messages
get_history get_history
iter_history
send_poll send_poll
vote_poll vote_poll
retract_vote retract_vote
@ -91,7 +92,9 @@ Chats
get_chat_member get_chat_member
get_chat_members get_chat_members
get_chat_members_count get_chat_members_count
iter_chat_members
get_dialogs get_dialogs
iter_dialogs
Users Users
----- -----

View File

@ -26,7 +26,7 @@ except ImportError:
else: else:
asyncio.set_event_loop_policy(uvloop.EventLoopPolicy()) asyncio.set_event_loop_policy(uvloop.EventLoopPolicy())
__copyright__ = "Copyright (C) 2017-2018 Dan Tès <https://github.com/delivrance>".replace( __copyright__ = "Copyright (C) 2017-2019 Dan Tès <https://github.com/delivrance>".replace(
"\xe8", "\xe8",
"e" if sys.getfilesystemencoding() != "utf-8" else "\xe8" "e" if sys.getfilesystemencoding() != "utf-8" else "\xe8"
) )
@ -40,7 +40,7 @@ from .client.types import (
Location, Message, MessageEntity, Dialog, Dialogs, Photo, PhotoSize, Sticker, User, UserStatus, Location, Message, MessageEntity, Dialog, Dialogs, Photo, PhotoSize, Sticker, User, UserStatus,
UserProfilePhotos, Venue, Animation, Video, VideoNote, Voice, CallbackQuery, Messages, ForceReply, UserProfilePhotos, Venue, Animation, Video, VideoNote, Voice, CallbackQuery, Messages, ForceReply,
InlineKeyboardButton, InlineKeyboardMarkup, KeyboardButton, ReplyKeyboardMarkup, ReplyKeyboardRemove, InlineKeyboardButton, InlineKeyboardMarkup, KeyboardButton, ReplyKeyboardMarkup, ReplyKeyboardRemove,
Poll, PollOption, ChatPreview, StopPropagation Poll, PollOption, ChatPreview, StopPropagation, Game
) )
from .client import ( from .client import (
Client, ChatAction, ParseMode, Emoji, Client, ChatAction, ParseMode, Emoji,

View File

@ -153,6 +153,19 @@ class Client(Methods, BaseClient):
Define a custom directory for your plugins. The plugins directory is the location in your Define a custom directory for your plugins. The plugins directory is the location in your
filesystem where Pyrogram will automatically load your update handlers. filesystem where Pyrogram will automatically load your update handlers.
Defaults to None (plugins disabled). Defaults to None (plugins disabled).
no_updates (``bool``, *optional*):
Pass True to completely disable incoming updates for the current session.
When updates are disabled your client can't receive any new message.
Useful for batch programs that don't need to deal with updates.
Defaults to False (updates enabled and always received).
takeout (``bool``, *optional*):
Pass True to let the client use a takeout session instead of a normal one, implies no_updates.
Useful for exporting your Telegram data. Methods invoked inside a takeout session (such as get_history,
download_media, ...) are less prone to throw FloodWait exceptions.
Only available for users, bots will ignore this parameter.
Defaults to False (normal session).
""" """
def __init__(self, def __init__(self,
@ -175,7 +188,9 @@ class Client(Methods, BaseClient):
workers: int = BaseClient.WORKERS, workers: int = BaseClient.WORKERS,
workdir: str = BaseClient.WORKDIR, workdir: str = BaseClient.WORKDIR,
config_file: str = BaseClient.CONFIG_FILE, config_file: str = BaseClient.CONFIG_FILE,
plugins_dir: str = None): plugins_dir: str = None,
no_updates: bool = None,
takeout: bool = None):
super().__init__() super().__init__()
self.session_name = session_name self.session_name = session_name
@ -199,6 +214,8 @@ class Client(Methods, BaseClient):
self.workdir = workdir self.workdir = workdir
self.config_file = config_file self.config_file = config_file
self.plugins_dir = plugins_dir self.plugins_dir = plugins_dir
self.no_updates = no_updates
self.takeout = takeout
self.dispatcher = Dispatcher(self, workers) self.dispatcher = Dispatcher(self, workers)
@ -255,6 +272,10 @@ class Client(Methods, BaseClient):
self.save_session() self.save_session()
if self.bot_token is None: if self.bot_token is None:
if self.takeout:
self.takeout_id = (await self.send(functions.account.InitTakeoutSession())).id
log.warning("Takeout session {} initiated".format(self.takeout_id))
now = time.time() now = time.time()
if abs(now - self.date) > Client.OFFLINE_SLEEP: if abs(now - self.date) > Client.OFFLINE_SLEEP:
@ -299,6 +320,10 @@ class Client(Methods, BaseClient):
if not self.is_started: if not self.is_started:
raise ConnectionError("Client is already stopped") raise ConnectionError("Client is already stopped")
if self.takeout_id:
self.send(functions.account.FinishTakeoutSession())
log.warning("Takeout session {} finished".format(self.takeout_id))
await Syncer.remove(self) await Syncer.remove(self)
await self.dispatcher.stop() await self.dispatcher.stop()
@ -944,6 +969,12 @@ class Client(Methods, BaseClient):
if not self.is_started: if not self.is_started:
raise ConnectionError("Client has not been started") raise ConnectionError("Client has not been started")
if self.no_updates:
data = functions.InvokeWithoutUpdates(data)
if self.takeout_id:
data = functions.InvokeWithTakeout(self.takeout_id, data)
r = await self.session.send(data, retries, timeout) r = await self.session.send(data, retries, timeout)
self.fetch_peers(getattr(r, "users", [])) self.fetch_peers(getattr(r, "users", []))

View File

@ -22,8 +22,6 @@ import re
from pyrogram import __version__ from pyrogram import __version__
from ..style import Markdown, HTML from ..style import Markdown, HTML
from ...api.core import Object
from ...session import Session
from ...session.internals import MsgId from ...session.internals import MsgId
@ -89,6 +87,8 @@ class BaseClient:
self.is_started = None self.is_started = None
self.is_idle = None self.is_idle = None
self.takeout_id = None
self.updates_queue = asyncio.Queue() self.updates_queue = asyncio.Queue()
self.updates_worker_task = None self.updates_worker_task = None
self.download_queue = asyncio.Queue() self.download_queue = asyncio.Queue()
@ -96,33 +96,29 @@ class BaseClient:
self.disconnect_handler = None self.disconnect_handler = None
def send(self, data: Object, retries: int = Session.MAX_RETRIES, timeout: float = Session.WAIT_TIMEOUT): def send(self, *args, **kwargs):
pass pass
def resolve_peer(self, peer_id: int or str): def resolve_peer(self, *args, **kwargs):
pass pass
def fetch_peers(self, entities): def fetch_peers(self, *args, **kwargs):
pass pass
def add_handler(self, handler, group: int = 0) -> tuple: def add_handler(self, *args, **kwargs):
pass pass
def save_file( def save_file(self, *args, **kwargs):
self,
path: str,
file_id: int = None,
file_part: int = 0,
progress: callable = None,
progress_args: tuple = ()
):
pass pass
def get_messages( def get_messages(self, *args, **kwargs):
self, pass
chat_id: int or str,
message_ids: int or list = None, def get_history(self, *args, **kwargs):
reply_to_message_ids: int or list = None, pass
replies: int = 1
): def get_dialogs(self, *args, **kwargs):
pass
def get_chat_members(self, *args, **kwargs):
pass pass

View File

@ -61,6 +61,9 @@ class Filters:
create = create create = create
me = create("Me", lambda _, m: bool(m.from_user and m.from_user.is_self))
"""Filter messages coming from you yourself"""
bot = create("Bot", lambda _, m: bool(m.from_user and m.from_user.is_bot)) 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"""
@ -97,9 +100,12 @@ class Filters:
sticker = create("Sticker", lambda _, m: bool(m.sticker)) sticker = create("Sticker", lambda _, m: bool(m.sticker))
"""Filter messages that contain :obj:`Sticker <pyrogram.Sticker>` objects.""" """Filter messages that contain :obj:`Sticker <pyrogram.Sticker>` objects."""
animation = create("GIF", lambda _, m: bool(m.animation)) animation = create("Animation", lambda _, m: bool(m.animation))
"""Filter messages that contain :obj:`Animation <pyrogram.Animation>` objects.""" """Filter messages that contain :obj:`Animation <pyrogram.Animation>` objects."""
game = create("Game", lambda _, m: bool(m.game))
"""Filter messages that contain :obj:`Game <pyrogram.Game>` objects."""
video = create("Video", lambda _, m: bool(m.video)) video = create("Video", lambda _, m: bool(m.video))
"""Filter messages that contain :obj:`Video <pyrogram.Video>` objects.""" """Filter messages that contain :obj:`Video <pyrogram.Video>` objects."""
@ -166,6 +172,9 @@ class Filters:
pinned_message = create("PinnedMessage", lambda _, m: bool(m.pinned_message)) pinned_message = create("PinnedMessage", lambda _, m: bool(m.pinned_message))
"""Filter service messages for pinned messages.""" """Filter service messages for pinned messages."""
game_score = create("GameScore", lambda _, m: bool(m.game_score))
"""Filter service messages for game scores."""
reply_keyboard = create("ReplyKeyboard", lambda _, m: isinstance(m.reply_markup, ReplyKeyboardMarkup)) reply_keyboard = create("ReplyKeyboard", lambda _, m: isinstance(m.reply_markup, ReplyKeyboardMarkup))
"""Filter messages containing reply keyboard markups""" """Filter messages containing reply keyboard markups"""
@ -190,7 +199,8 @@ class Filters:
- channel_chat_created - channel_chat_created
- migrate_to_chat_id - migrate_to_chat_id
- migrate_from_chat_id - migrate_from_chat_id
- pinned_message""" - pinned_message
- game_score"""
media = create("Media", lambda _, m: bool(m.media)) media = create("Media", lambda _, m: bool(m.media))
"""Filter media messages. A media message contains any of the following fields set """Filter media messages. A media message contains any of the following fields set
@ -205,7 +215,8 @@ class Filters:
- video_note - video_note
- contact - contact
- location - location
- venue""" - venue
- poll"""
@staticmethod @staticmethod
def command(command: str or list, def command(command: str or list,

View File

@ -24,6 +24,8 @@ from .get_chat_members import GetChatMembers
from .get_chat_members_count import GetChatMembersCount from .get_chat_members_count import GetChatMembersCount
from .get_chat_preview import GetChatPreview from .get_chat_preview import GetChatPreview
from .get_dialogs import GetDialogs from .get_dialogs import GetDialogs
from .iter_chat_members import IterChatMembers
from .iter_dialogs import IterDialogs
from .join_chat import JoinChat from .join_chat import JoinChat
from .kick_chat_member import KickChatMember from .kick_chat_member import KickChatMember
from .leave_chat import LeaveChat from .leave_chat import LeaveChat
@ -56,6 +58,8 @@ class Chats(
UnpinChatMessage, UnpinChatMessage,
GetDialogs, GetDialogs,
GetChatMembersCount, GetChatMembersCount,
GetChatPreview GetChatPreview,
IterDialogs,
IterChatMembers
): ):
pass pass

View File

@ -39,10 +39,12 @@ class GetChatMembers(BaseClient):
limit: int = 200, limit: int = 200,
query: str = "", query: str = "",
filter: str = Filters.ALL) -> "pyrogram.ChatMembers": filter: str = Filters.ALL) -> "pyrogram.ChatMembers":
"""Use this method to get the members list of a chat. """Use this method to get a chunk of the members list of a chat.
You can get up to 200 chat members at once.
A chat can be either a basic group, a supergroup or a channel. A chat can be either a basic group, a supergroup or a channel.
You must be admin to retrieve the members list of a channel (also known as "subscribers"). You must be admin to retrieve the members list of a channel (also known as "subscribers").
For a more convenient way of getting chat members see :meth:`iter_chat_members`.
Args: Args:
chat_id (``int`` | ``str``): chat_id (``int`` | ``str``):

View File

@ -16,19 +16,26 @@
# You should have received a copy of the GNU Lesser General Public License # You should have received a copy of the GNU Lesser General Public License
# along with Pyrogram. If not, see <http://www.gnu.org/licenses/>. # along with Pyrogram. If not, see <http://www.gnu.org/licenses/>.
import logging
import time
import pyrogram import pyrogram
from pyrogram.api import functions, types from pyrogram.api import functions, types
from pyrogram.api.errors import FloodWait
from ...ext import BaseClient from ...ext import BaseClient
log = logging.getLogger(__name__)
class GetDialogs(BaseClient): class GetDialogs(BaseClient):
async def get_dialogs(self, async def get_dialogs(self,
offset_date: int = 0, offset_date: int = 0,
limit: int = 100, limit: int = 100,
pinned_only: bool = False) -> "pyrogram.Dialogs": pinned_only: bool = False) -> "pyrogram.Dialogs":
"""Use this method to get the user's dialogs """Use this method to get a chunk of the user's dialogs
You can get up to 100 dialogs at once. You can get up to 100 dialogs at once.
For a more convenient way of getting a user's dialogs see :meth:`iter_dialogs`.
Args: Args:
offset_date (``int``): offset_date (``int``):
@ -50,6 +57,8 @@ class GetDialogs(BaseClient):
:class:`Error <pyrogram.Error>` in case of a Telegram RPC error. :class:`Error <pyrogram.Error>` in case of a Telegram RPC error.
""" """
while True:
try:
if pinned_only: if pinned_only:
r = await self.send(functions.messages.GetPinnedDialogs()) r = await self.send(functions.messages.GetPinnedDialogs())
else: else:
@ -63,5 +72,10 @@ class GetDialogs(BaseClient):
exclude_pinned=True exclude_pinned=True
) )
) )
except FloodWait as e:
log.warning("Sleeping {}s".format(e.x))
time.sleep(e.x)
else:
break
return pyrogram.Dialogs._parse(self, r) return pyrogram.Dialogs._parse(self, r)

View File

@ -0,0 +1,124 @@
# 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 string import ascii_lowercase
from typing import Union, Generator
import pyrogram
from ...ext import BaseClient
class Filters:
ALL = "all"
KICKED = "kicked"
RESTRICTED = "restricted"
BOTS = "bots"
RECENT = "recent"
ADMINISTRATORS = "administrators"
QUERIES = [""] + [str(i) for i in range(10)] + list(ascii_lowercase)
QUERYABLE_FILTERS = (Filters.ALL, Filters.KICKED, Filters.RESTRICTED)
class IterChatMembers(BaseClient):
def iter_chat_members(self,
chat_id: Union[int, str],
limit: int = 0,
query: str = "",
filter: str = Filters.ALL) -> Generator["pyrogram.ChatMember", None, None]:
"""Use this method to iterate through the members of a chat sequentially.
This convenience method does the same as repeatedly calling :meth:`get_chat_members` in a loop, thus saving you
from the hassle of setting up boilerplate code. It is useful for getting the whole members list of a chat with
a single call.
Args:
chat_id (``int`` | ``str``):
Unique identifier (int) or username (str) of the target chat.
limit (``int``, *optional*):
Limits the number of members to be retrieved.
By default, no limit is applied and all members are returned.
query (``str``, *optional*):
Query string to filter members based on their display names and usernames.
Defaults to "" (empty string) [2]_.
filter (``str``, *optional*):
Filter used to select the kind of members you want to retrieve. Only applicable for supergroups
and channels. It can be any of the followings:
*"all"* - all kind of members,
*"kicked"* - kicked (banned) members only,
*"restricted"* - restricted members only,
*"bots"* - bots only,
*"recent"* - recent members only,
*"administrators"* - chat administrators only.
Defaults to *"all"*.
.. [1] Server limit: on supergroups, you can get up to 10,000 members for a single query and up to 200 members
on channels.
.. [2] A query string is applicable only for *"all"*, *"kicked"* and *"restricted"* filters only.
Returns:
A generator yielding :obj:`ChatMember <pyrogram.ChatMember>` objects.
Raises:
:class:`Error <pyrogram.Error>` in case of a Telegram RPC error.
"""
current = 0
yielded = set()
queries = [query] if query else QUERIES
total = limit or (1 << 31) - 1
limit = min(200, total)
if filter not in QUERYABLE_FILTERS:
queries = [""]
for q in queries:
offset = 0
while True:
chat_members = self.get_chat_members(
chat_id=chat_id,
offset=offset,
limit=limit,
query=q,
filter=filter
).chat_members
if not chat_members:
break
offset += len(chat_members)
for chat_member in chat_members:
user_id = chat_member.user.id
if user_id in yielded:
continue
yield chat_member
yielded.add(chat_member.user.id)
current += 1
if current >= total:
return

View File

@ -0,0 +1,82 @@
# 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 Generator
import pyrogram
from ...ext import BaseClient
class IterDialogs(BaseClient):
def iter_dialogs(self,
offset_date: int = 0,
limit: int = 0) -> Generator["pyrogram.Dialog", None, None]:
"""Use this method to iterate through a user's dialogs sequentially.
This convenience method does the same as repeatedly calling :meth:`get_dialogs` in a loop, thus saving you from
the hassle of setting up boilerplate code. It is useful for getting the whole dialogs list with a single call.
Args:
offset_date (``int``):
The offset date in Unix time taken from the top message of a :obj:`Dialog`.
Defaults to 0 (most recent dialog).
limit (``str``, *optional*):
Limits the number of dialogs to be retrieved.
By default, no limit is applied and all dialogs are returned.
Returns:
A generator yielding :obj:`Dialog <pyrogram.Dialog>` objects.
Raises:
:class:`Error <pyrogram.Error>` in case of a Telegram RPC error.
"""
current = 0
total = limit or (1 << 31) - 1
limit = min(100, total)
pinned_dialogs = self.get_dialogs(
pinned_only=True
).dialogs
for dialog in pinned_dialogs:
yield dialog
current += 1
if current >= total:
return
while True:
dialogs = self.get_dialogs(
offset_date=offset_date,
limit=limit
).dialogs
if not dialogs:
return
offset_date = dialogs[-1].top_message.date
for dialog in dialogs:
yield dialog
current += 1
if current >= total:
return

View File

@ -27,7 +27,7 @@ class KickChatMember(BaseClient):
async def kick_chat_member(self, async def kick_chat_member(self,
chat_id: Union[int, str], chat_id: Union[int, str],
user_id: Union[int, str], user_id: Union[int, str],
until_date: int = 0) -> "pyrogram.Message": until_date: int = 0) -> Union["pyrogram.Message", bool]:
"""Use this method to kick a user from a group, a supergroup or a channel. """Use this method to kick a user from a group, a supergroup or a channel.
In the case of supergroups and channels, the user will not be able to return to the group on their own using In the case of supergroups and channels, the user will not be able to return to the group on their own using
invite links, etc., unless unbanned first. You must be an administrator in the chat for this to work and must invite links, etc., unless unbanned first. You must be an administrator in the chat for this to work and must
@ -52,7 +52,7 @@ class KickChatMember(BaseClient):
considered to be banned forever. Defaults to 0 (ban forever). considered to be banned forever. Defaults to 0 (ban forever).
Returns: Returns:
True on success. On success, either True or a service :obj:`Message <pyrogram.Message>` will be returned (when applicable).
Raises: Raises:
:class:`Error <pyrogram.Error>` in case of a Telegram RPC error. :class:`Error <pyrogram.Error>` in case of a Telegram RPC error.
@ -93,3 +93,5 @@ class KickChatMember(BaseClient):
{i.id: i for i in r.users}, {i.id: i for i in r.users},
{i.id: i for i in r.chats} {i.id: i for i in r.chats}
) )
else:
return True

View File

@ -16,6 +16,7 @@
# You should have received a copy of the GNU Lesser General Public License # You should have received a copy of the GNU Lesser General Public License
# along with Pyrogram. If not, see <http://www.gnu.org/licenses/>. # along with Pyrogram. If not, see <http://www.gnu.org/licenses/>.
from .close_poll import ClosePoll
from .delete_messages import DeleteMessages from .delete_messages import DeleteMessages
from .download_media import DownloadMedia from .download_media import DownloadMedia
from .edit_message_caption import EditMessageCaption from .edit_message_caption import EditMessageCaption
@ -25,6 +26,7 @@ from .edit_message_text import EditMessageText
from .forward_messages import ForwardMessages from .forward_messages import ForwardMessages
from .get_history import GetHistory from .get_history import GetHistory
from .get_messages import GetMessages from .get_messages import GetMessages
from .iter_history import IterHistory
from .retract_vote import RetractVote from .retract_vote import RetractVote
from .send_animation import SendAnimation from .send_animation import SendAnimation
from .send_audio import SendAudio from .send_audio import SendAudio
@ -69,7 +71,9 @@ class Messages(
SendVoice, SendVoice,
SendPoll, SendPoll,
VotePoll, VotePoll,
ClosePoll,
RetractVote, RetractVote,
DownloadMedia DownloadMedia,
IterHistory
): ):
pass pass

View File

@ -0,0 +1,65 @@
# 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 pyrogram.client.ext import BaseClient
class ClosePoll(BaseClient):
def close_poll(self,
chat_id: Union[int, str],
message_id: id) -> bool:
"""Use this method to close (stop) a poll.
Closed polls can't be reopened and nobody will be able to vote in it anymore.
Args:
chat_id (``int`` | ``str``):
Unique identifier (int) or username (str) of the target chat.
For your personal cloud (Saved Messages) you can simply use "me" or "self".
For a contact that exists in your Telegram address book you can use his phone number (str).
message_id (``int``):
Unique poll message identifier inside this chat.
Returns:
On success, True is returned.
Raises:
:class:`Error <pyrogram.Error>` in case of a Telegram RPC error.
"""
poll = self.get_messages(chat_id, message_id).poll
self.send(
functions.messages.EditMessage(
peer=self.resolve_peer(chat_id),
id=message_id,
media=types.InputMediaPoll(
poll=types.Poll(
id=poll.id,
closed=True,
question="",
answers=[]
)
)
)
)
return True

View File

@ -30,10 +30,11 @@ class GetHistory(BaseClient):
offset: int = 0, offset: int = 0,
offset_id: int = 0, offset_id: int = 0,
offset_date: int = 0, offset_date: int = 0,
reversed: bool = False): reverse: bool = False):
"""Use this method to retrieve the history of a chat. """Use this method to retrieve a chunk of the history of a chat.
You can get up to 100 messages at once. You can get up to 100 messages at once.
For a more convenient way of getting a chat history see :meth:`iter_history`.
Args: Args:
chat_id (``int`` | ``str``): chat_id (``int`` | ``str``):
@ -55,7 +56,7 @@ class GetHistory(BaseClient):
offset_date (``int``, *optional*): offset_date (``int``, *optional*):
Pass a date in Unix time as offset to retrieve only older messages starting from that date. Pass a date in Unix time as offset to retrieve only older messages starting from that date.
reversed (``bool``, *optional*): reverse (``bool``, *optional*):
Pass True to retrieve the messages in reversed order (from older to most recent). Pass True to retrieve the messages in reversed order (from older to most recent).
Returns: Returns:
@ -72,7 +73,7 @@ class GetHistory(BaseClient):
peer=await self.resolve_peer(chat_id), peer=await self.resolve_peer(chat_id),
offset_id=offset_id, offset_id=offset_id,
offset_date=offset_date, offset_date=offset_date,
add_offset=offset - (limit if reversed else 0), add_offset=offset * (-1 if reverse else 1) - (limit if reverse else 0),
limit=limit, limit=limit,
max_id=0, max_id=0,
min_id=0, min_id=0,
@ -81,7 +82,7 @@ class GetHistory(BaseClient):
) )
) )
if reversed: if reverse:
messages.messages.reverse() messages.messages.reverse()
return messages return messages

View File

@ -16,19 +16,24 @@
# You should have received a copy of the GNU Lesser General Public License # You should have received a copy of the GNU Lesser General Public License
# along with Pyrogram. If not, see <http://www.gnu.org/licenses/>. # along with Pyrogram. If not, see <http://www.gnu.org/licenses/>.
import logging
import time
from typing import Union, Iterable from typing import Union, Iterable
import pyrogram import pyrogram
from pyrogram.api import functions, types from pyrogram.api import functions, types
from pyrogram.api.errors import FloodWait
from ...ext import BaseClient from ...ext import BaseClient
log = logging.getLogger(__name__)
class GetMessages(BaseClient): class GetMessages(BaseClient):
async def get_messages(self, async def get_messages(self,
chat_id: Union[int, str], chat_id: Union[int, str],
message_ids: Union[int, Iterable[int]] = None, message_ids: Union[int, Iterable[int]] = None,
reply_to_message_ids: Union[int, Iterable[int]] = None, reply_to_message_ids: Union[int, Iterable[int]] = None,
replies: int = 1) -> "pyrogram.Messages": replies: int = 1) -> Union["pyrogram.Message", "pyrogram.Messages"]:
"""Use this method to get one or more messages that belong to a specific chat. """Use this method to get one or more messages that belong to a specific chat.
You can retrieve up to 200 messages at once. You can retrieve up to 200 messages at once.
@ -78,6 +83,15 @@ class GetMessages(BaseClient):
else: else:
rpc = functions.messages.GetMessages(id=ids) rpc = functions.messages.GetMessages(id=ids)
messages = await pyrogram.Messages._parse(self, await self.send(rpc), replies) while True:
try:
r = await self.send(rpc)
except FloodWait as e:
log.warning("Sleeping for {}s".format(e.x))
time.sleep(e.x)
else:
break
messages = await pyrogram.Messages._parse(self, r, replies)
return messages if is_iterable else messages.messages[0] return messages if is_iterable else messages.messages[0]

View File

@ -0,0 +1,93 @@
# 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, Generator
import pyrogram
from ...ext import BaseClient
class IterHistory(BaseClient):
def iter_history(self,
chat_id: Union[int, str],
limit: int = 0,
offset: int = 0,
offset_id: int = 0,
offset_date: int = 0,
reverse: bool = False) -> Generator["pyrogram.Message", None, None]:
"""Use this method to iterate through a chat history sequentially.
This convenience method does the same as repeatedly calling :meth:`get_history` in a loop, thus saving you from
the hassle of setting up boilerplate code. It is useful for getting the whole chat history with a single call.
Args:
chat_id (``int`` | ``str``):
Unique identifier (int) or username (str) of the target chat.
For your personal cloud (Saved Messages) you can simply use "me" or "self".
For a contact that exists in your Telegram address book you can use his phone number (str).
limit (``int``, *optional*):
Limits the number of messages to be retrieved.
By default, no limit is applied and all messages are returned.
offset (``int``, *optional*):
Sequential number of the first message to be returned..
Negative values are also accepted and become useful in case you set offset_id or offset_date.
offset_id (``int``, *optional*):
Identifier of the first message to be returned.
offset_date (``int``, *optional*):
Pass a date in Unix time as offset to retrieve only older messages starting from that date.
reverse (``bool``, *optional*):
Pass True to retrieve the messages in reversed order (from older to most recent).
Returns:
A generator yielding :obj:`Message <pyrogram.Message>` objects.
Raises:
:class:`Error <pyrogram.Error>` in case of a Telegram RPC error.
"""
offset_id = offset_id or (1 if reverse else 0)
current = 0
total = limit or (1 << 31) - 1
limit = min(100, total)
while True:
messages = self.get_history(
chat_id=chat_id,
limit=limit,
offset=offset,
offset_id=offset_id,
offset_date=offset_date,
reverse=reverse
).messages
if not messages:
return
offset_id = messages[-1].message_id + (1 if reverse else 0)
for message in messages:
yield message
current += 1
if current >= total:
return

View File

@ -38,12 +38,12 @@ class HTML:
def __init__(self, peers_by_id): def __init__(self, peers_by_id):
self.peers_by_id = peers_by_id self.peers_by_id = peers_by_id
def parse(self, text): def parse(self, message: str):
entities = [] entities = []
text = utils.add_surrogates(text) message = utils.add_surrogates(str(message))
offset = 0 offset = 0
for match in self.HTML_RE.finditer(text): for match in self.HTML_RE.finditer(message):
start = match.start() - offset start = match.start() - offset
style, url, body = match.group(1, 3, 4) style, url, body = match.group(1, 3, 4)
@ -73,12 +73,12 @@ class HTML:
continue continue
entities.append(entity) entities.append(entity)
text = text.replace(match.group(), body) message = message.replace(match.group(), body)
offset += len(style) * 2 + 5 + (len(url) + 8 if url else 0) offset += len(style) * 2 + 5 + (len(url) + 8 if url else 0)
# TODO: OrderedDict to be removed in Python3.6 # TODO: OrderedDict to be removed in Python3.6
return OrderedDict([ return OrderedDict([
("message", utils.remove_surrogates(text)), ("message", utils.remove_surrogates(message)),
("entities", entities) ("entities", entities)
]) ])

View File

@ -56,7 +56,7 @@ class Markdown:
self.peers_by_id = peers_by_id self.peers_by_id = peers_by_id
def parse(self, message: str): def parse(self, message: str):
message = utils.add_surrogates(message).strip() message = utils.add_surrogates(str(message)).strip()
entities = [] entities = []
offset = 0 offset = 0

View File

@ -31,7 +31,7 @@ from .input_media import (
from .messages_and_media import ( from .messages_and_media import (
Audio, Contact, Document, Animation, Location, Photo, PhotoSize, Audio, Contact, Document, Animation, Location, Photo, PhotoSize,
Sticker, Venue, Video, VideoNote, Voice, UserProfilePhotos, Sticker, Venue, Video, VideoNote, Voice, UserProfilePhotos,
Message, Messages, MessageEntity, Poll, PollOption Message, Messages, MessageEntity, Poll, PollOption, Game
) )
from .user_and_chats import ( from .user_and_chats import (
Chat, ChatMember, ChatMembers, ChatPhoto, Chat, ChatMember, ChatMembers, ChatPhoto,

View File

@ -23,3 +23,4 @@ from .inline_keyboard_markup import InlineKeyboardMarkup
from .keyboard_button import KeyboardButton from .keyboard_button import KeyboardButton
from .reply_keyboard_markup import ReplyKeyboardMarkup from .reply_keyboard_markup import ReplyKeyboardMarkup
from .reply_keyboard_remove import ReplyKeyboardRemove from .reply_keyboard_remove import ReplyKeyboardRemove
from .callback_game import CallbackGame

View File

@ -0,0 +1,29 @@
# 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 ..pyrogram_type import PyrogramType
class CallbackGame(PyrogramType):
"""A placeholder, currently holds no information.
Use BotFather to set up your game.
"""
def __init__(self):
super().__init__(None)

View File

@ -18,8 +18,9 @@
from pyrogram.api.types import ( from pyrogram.api.types import (
KeyboardButtonUrl, KeyboardButtonCallback, KeyboardButtonUrl, KeyboardButtonCallback,
KeyboardButtonSwitchInline KeyboardButtonSwitchInline, KeyboardButtonGame
) )
from .callback_game import CallbackGame
from ..pyrogram_type import PyrogramType from ..pyrogram_type import PyrogramType
@ -58,7 +59,8 @@ class InlineKeyboardButton(PyrogramType):
callback_data: bytes = None, callback_data: bytes = None,
url: str = None, url: str = None,
switch_inline_query: str = None, switch_inline_query: str = None,
switch_inline_query_current_chat: str = None): switch_inline_query_current_chat: str = None,
callback_game: CallbackGame = None):
super().__init__(None) super().__init__(None)
self.text = text self.text = text
@ -66,7 +68,7 @@ class InlineKeyboardButton(PyrogramType):
self.callback_data = callback_data self.callback_data = callback_data
self.switch_inline_query = switch_inline_query self.switch_inline_query = switch_inline_query
self.switch_inline_query_current_chat = switch_inline_query_current_chat self.switch_inline_query_current_chat = switch_inline_query_current_chat
# self.callback_game = callback_game self.callback_game = callback_game
# self.pay = pay # self.pay = pay
@staticmethod @staticmethod
@ -95,6 +97,12 @@ class InlineKeyboardButton(PyrogramType):
switch_inline_query=o.query switch_inline_query=o.query
) )
if isinstance(o, KeyboardButtonGame):
return InlineKeyboardButton(
text=o.text,
callback_game=CallbackGame()
)
def write(self): def write(self):
if self.callback_data: if self.callback_data:
return KeyboardButtonCallback(self.text, self.callback_data) return KeyboardButtonCallback(self.text, self.callback_data)
@ -107,3 +115,6 @@ class InlineKeyboardButton(PyrogramType):
if self.switch_inline_query_current_chat: if self.switch_inline_query_current_chat:
return KeyboardButtonSwitchInline(self.text, self.switch_inline_query_current_chat, same_peer=True) return KeyboardButtonSwitchInline(self.text, self.switch_inline_query_current_chat, same_peer=True)
if self.callback_game:
return KeyboardButtonGame(self.text)

View File

@ -20,7 +20,7 @@ from . import InputMedia
class InputMediaAudio(InputMedia): class InputMediaAudio(InputMedia):
"""This object represents a video to be sent inside an album. """This object represents an audio to be sent inside an album.
It is intended to be used with :obj:`send_media_group() <pyrogram.Client.send_media_group>`. It is intended to be used with :obj:`send_media_group() <pyrogram.Client.send_media_group>`.
Args: Args:

View File

@ -34,3 +34,4 @@ from .venue import Venue
from .video import Video from .video import Video
from .video_note import VideoNote from .video_note import VideoNote
from .voice import Voice from .voice import Voice
from .game import Game

View File

@ -0,0 +1,98 @@
# 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/>.
import pyrogram
from pyrogram.api import types
from .animation import Animation
from .photo import Photo
from ..pyrogram_type import PyrogramType
class Game(PyrogramType):
"""This object represents a game.
Use BotFather to create and edit games, their short names will act as unique identifiers.
Args:
id (``int``):
Unique identifier of the game.
title (``str``):
Title of the game.
short_name (``str``):
Unique short name of the game.
description (``str``):
Description of the game.
photo (:obj:`Photo <pyrogram.Photo>`):
Photo that will be displayed in the game message in chats.
animation (:obj:`Animation <pyrogram.Animation>`, *optional*):
Animation that will be displayed in the game message in chats.
Upload via BotFather.
"""
def __init__(self,
*,
client: "pyrogram.client.ext.BaseClient",
id: int,
title: str,
short_name: str,
description: str,
photo: Photo,
animation: Animation = None):
super().__init__(client)
self.id = id
self.title = title
self.short_name = short_name
self.description = description
self.photo = photo
self.animation = animation
@staticmethod
def _parse(client, message: types.Message) -> "Game":
game = message.media.game # type: types.Game
animation = None
if game.document:
attributes = {type(i): i for i in game.document.attributes}
file_name = getattr(
attributes.get(
types.DocumentAttributeFilename, None
), "file_name", None
)
animation = Animation._parse(
client,
game.document,
attributes.get(types.DocumentAttributeVideo, None),
file_name
)
return Game(
id=game.id,
title=game.title,
short_name=game.short_name,
description=game.description,
photo=Photo._parse(client, game.photo),
animation=animation,
client=client
)

View File

@ -121,6 +121,9 @@ class Message(PyrogramType, Update):
animation (:obj:`Animation <pyrogram.Animation>`, *optional*): animation (:obj:`Animation <pyrogram.Animation>`, *optional*):
Message is an animation, information about the animation. Message is an animation, information about the animation.
game (:obj:`Game <pyrogram.Game>`, *optional*):
Message is a game, information about the game.
video (:obj:`Video <pyrogram.Video>`, *optional*): video (:obj:`Video <pyrogram.Video>`, *optional*):
Message is a video, information about the video. Message is a video, information about the video.
@ -199,6 +202,10 @@ class Message(PyrogramType, Update):
Note that the Message object in this field will not contain further reply_to_message fields even if it Note that the Message object in this field will not contain further reply_to_message fields even if it
is itself a reply. is itself a reply.
game_score (``int``, *optional*):
The game score for a user.
The reply_to_message field will contain the game Message.
views (``int``, *optional*): views (``int``, *optional*):
Channel post views. Channel post views.
@ -255,6 +262,7 @@ class Message(PyrogramType, Update):
photo: "pyrogram.Photo" = None, photo: "pyrogram.Photo" = None,
sticker: "pyrogram.Sticker" = None, sticker: "pyrogram.Sticker" = None,
animation: "pyrogram.Animation" = None, animation: "pyrogram.Animation" = None,
game: "pyrogram.Game" = None,
video: "pyrogram.Video" = None, video: "pyrogram.Video" = None,
voice: "pyrogram.Voice" = None, voice: "pyrogram.Voice" = None,
video_note: "pyrogram.VideoNote" = None, video_note: "pyrogram.VideoNote" = None,
@ -275,6 +283,7 @@ class Message(PyrogramType, Update):
migrate_to_chat_id: int = None, migrate_to_chat_id: int = None,
migrate_from_chat_id: int = None, migrate_from_chat_id: int = None,
pinned_message: "Message" = None, pinned_message: "Message" = None,
game_score: int = None,
views: int = None, views: int = None,
via_bot: User = None, via_bot: User = None,
outgoing: bool = None, outgoing: bool = None,
@ -311,6 +320,7 @@ class Message(PyrogramType, Update):
self.photo = photo self.photo = photo
self.sticker = sticker self.sticker = sticker
self.animation = animation self.animation = animation
self.game = game
self.video = video self.video = video
self.voice = voice self.voice = voice
self.video_note = video_note self.video_note = video_note
@ -331,6 +341,7 @@ class Message(PyrogramType, Update):
self.migrate_to_chat_id = migrate_to_chat_id self.migrate_to_chat_id = migrate_to_chat_id
self.migrate_from_chat_id = migrate_from_chat_id self.migrate_from_chat_id = migrate_from_chat_id
self.pinned_message = pinned_message self.pinned_message = pinned_message
self.game_score = game_score
self.views = views self.views = views
self.via_bot = via_bot self.via_bot = via_bot
self.outgoing = outgoing self.outgoing = outgoing
@ -407,6 +418,19 @@ class Message(PyrogramType, Update):
except MessageIdsEmpty: except MessageIdsEmpty:
pass pass
if isinstance(action, types.MessageActionGameScore):
parsed_message.game_score = action.score
if message.reply_to_msg_id and replies:
try:
parsed_message.reply_to_message = client.get_messages(
parsed_message.chat.id,
reply_to_message_ids=message.id,
replies=0
)
except MessageIdsEmpty:
pass
return parsed_message return parsed_message
if isinstance(message, types.Message): if isinstance(message, types.Message):
@ -435,6 +459,7 @@ class Message(PyrogramType, Update):
location = None location = None
contact = None contact = None
venue = None venue = None
game = None
audio = None audio = None
voice = None voice = None
animation = None animation = None
@ -456,6 +481,8 @@ class Message(PyrogramType, Update):
contact = Contact._parse(client, media) contact = Contact._parse(client, media)
elif isinstance(media, types.MessageMediaVenue): elif isinstance(media, types.MessageMediaVenue):
venue = pyrogram.Venue._parse(client, media) venue = pyrogram.Venue._parse(client, media)
elif isinstance(media, types.MessageMediaGame):
game = pyrogram.Game._parse(client, message)
elif isinstance(media, types.MessageMediaDocument): elif isinstance(media, types.MessageMediaDocument):
doc = media.document doc = media.document
@ -543,6 +570,7 @@ class Message(PyrogramType, Update):
audio=audio, audio=audio,
voice=voice, voice=voice,
animation=animation, animation=animation,
game=game,
video=video, video=video,
video_note=video_note, video_note=video_note,
sticker=sticker, sticker=sticker,

View File

@ -52,14 +52,41 @@ class Messages(PyrogramType, Update):
users = {i.id: i for i in messages.users} users = {i.id: i for i in messages.users}
chats = {i.id: i for i in messages.chats} chats = {i.id: i for i in messages.chats}
total_count = getattr(messages, "count", len(messages.messages))
if not messages.messages:
return Messages(
total_count=total_count,
messages=[],
client=client
)
# TODO: WTF! Py 3.5 doesn't support await inside comprehensions # TODO: WTF! Py 3.5 doesn't support await inside comprehensions
parsed_messages = [] parsed_messages = []
for message in messages.messages: for message in messages.messages:
parsed_messages.append(await Message._parse(client, message, users, chats, replies)) parsed_messages.appen(await Message._parse(client, message, users, chats, replies=0))
if replies:
messages_with_replies = {i.id: getattr(i, "reply_to_msg_id", None) for i in messages.messages}
reply_message_ids = [i[0] for i in filter(lambda x: x[1] is not None, messages_with_replies.items())]
if reply_message_ids:
reply_messages = (await client.get_messages(
parsed_messages[0].chat.id,
reply_to_message_ids=reply_message_ids,
replies=0
)).messages
for message in parsed_messages:
reply_id = messages_with_replies[message.message_id]
for reply in reply_messages:
if reply.message_id == reply_id:
message.reply_to_message = reply
return Messages( return Messages(
total_count=getattr(messages, "count", len(messages.messages)), total_count=total_count,
messages=parsed_messages, messages=parsed_messages,
client=client client=client
) )