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 (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+)`_
.. _`Telegram`: https://telegram.org/

View File

@ -26,7 +26,7 @@ NOTICE_PATH = "NOTICE"
SECTION_RE = re.compile(r"---(\w+)---")
LAYER_RE = re.compile(r"//\sLAYER\s(\d+)")
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_2 = re.compile(r"flags\.(\d+)\?([\w<>.]+)")
FLAGS_RE_3 = re.compile(r"flags:#")
@ -288,17 +288,20 @@ def start():
sorted_args = sort_args(c.args)
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 ""
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"
docstring_args = []
docs = c.docs.split("|")[1:] if c.docs else None
for i, arg in enumerate(sorted_args):
if arg == ("flags", "#"):
continue
arg_name, arg_type = arg
is_optional = FLAGS_RE.match(arg_type)
flag_number = is_optional.group(1) if is_optional else -1
@ -338,28 +341,31 @@ def start():
if references:
docstring_args += "\n\n See Also:\n This object can be returned by " + references + "."
if c.has_flags:
write_flags = []
for i in c.args:
flag = FLAGS_RE.match(i[1])
if flag:
write_flags.append("flags |= (1 << {}) if self.{} is not None else 0".format(flag.group(1), i[0]))
write_flags = "\n ".join([
"flags = 0",
"\n ".join(write_flags),
"b.write(Int(flags))"
])
else:
write_flags = "# No flags"
read_flags = "flags = Int.read(b)" if c.has_flags else "# No flags"
write_types = read_types = ""
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 = []
for i in c.args:
flag = FLAGS_RE.match(i[1])
if flag:
write_flags.append(
"flags |= (1 << {}) if self.{} is not None else 0".format(flag.group(1), i[0]))
write_flags = "\n ".join([
"flags = 0",
"\n ".join(write_flags),
"b.write(Int(flags))\n "
])
write_types += write_flags
read_types += "flags = Int.read(b)\n "
continue
if flag:
index, flag_type = flag[0]
@ -448,11 +454,9 @@ def start():
object_id=c.id,
arguments=arguments,
fields=fields,
read_flags=read_flags,
read_types=read_types,
write_flags=write_flags,
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
def read(b: BytesIO, *args) -> "{class_name}":
{read_flags}
{read_types}
return {class_name}({return_arguments})
@ -24,6 +23,5 @@ class {class_name}(Object):
b = BytesIO()
b.write(Int(self.ID, False))
{write_flags}
{write_types}
return b.getvalue()

View File

@ -80,4 +80,8 @@ USER_ADMIN_INVALID The action requires admin privileges
INPUT_USER_DEACTIVATED The target user has been deactivated
PASSWORD_RECOVERY_NA The password recovery e-mail is not available
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
80 INPUT_USER_DEACTIVATED The target user has been deactivated
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
get_messages
get_history
iter_history
send_poll
vote_poll
retract_vote
@ -91,7 +92,9 @@ Chats
get_chat_member
get_chat_members
get_chat_members_count
iter_chat_members
get_dialogs
iter_dialogs
Users
-----

View File

@ -26,7 +26,7 @@ except ImportError:
else:
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",
"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,
UserProfilePhotos, Venue, Animation, Video, VideoNote, Voice, CallbackQuery, Messages, ForceReply,
InlineKeyboardButton, InlineKeyboardMarkup, KeyboardButton, ReplyKeyboardMarkup, ReplyKeyboardRemove,
Poll, PollOption, ChatPreview, StopPropagation
Poll, PollOption, ChatPreview, StopPropagation, Game
)
from .client import (
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
filesystem where Pyrogram will automatically load your update handlers.
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,
@ -175,7 +188,9 @@ class Client(Methods, BaseClient):
workers: int = BaseClient.WORKERS,
workdir: str = BaseClient.WORKDIR,
config_file: str = BaseClient.CONFIG_FILE,
plugins_dir: str = None):
plugins_dir: str = None,
no_updates: bool = None,
takeout: bool = None):
super().__init__()
self.session_name = session_name
@ -199,6 +214,8 @@ class Client(Methods, BaseClient):
self.workdir = workdir
self.config_file = config_file
self.plugins_dir = plugins_dir
self.no_updates = no_updates
self.takeout = takeout
self.dispatcher = Dispatcher(self, workers)
@ -255,6 +272,10 @@ class Client(Methods, BaseClient):
self.save_session()
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()
if abs(now - self.date) > Client.OFFLINE_SLEEP:
@ -299,6 +320,10 @@ class Client(Methods, BaseClient):
if not self.is_started:
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 self.dispatcher.stop()
@ -944,6 +969,12 @@ class Client(Methods, BaseClient):
if not self.is_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)
self.fetch_peers(getattr(r, "users", []))

View File

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

View File

@ -61,6 +61,9 @@ 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"""
bot = create("Bot", lambda _, m: bool(m.from_user and m.from_user.is_bot))
"""Filter messages coming from bots"""
@ -97,9 +100,12 @@ class Filters:
sticker = create("Sticker", lambda _, m: bool(m.sticker))
"""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."""
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))
"""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))
"""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))
"""Filter messages containing reply keyboard markups"""
@ -190,7 +199,8 @@ class Filters:
- channel_chat_created
- migrate_to_chat_id
- migrate_from_chat_id
- pinned_message"""
- pinned_message
- game_score"""
media = create("Media", lambda _, m: bool(m.media))
"""Filter media messages. A media message contains any of the following fields set
@ -205,7 +215,8 @@ class Filters:
- video_note
- contact
- location
- venue"""
- venue
- poll"""
@staticmethod
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_preview import GetChatPreview
from .get_dialogs import GetDialogs
from .iter_chat_members import IterChatMembers
from .iter_dialogs import IterDialogs
from .join_chat import JoinChat
from .kick_chat_member import KickChatMember
from .leave_chat import LeaveChat
@ -56,6 +58,8 @@ class Chats(
UnpinChatMessage,
GetDialogs,
GetChatMembersCount,
GetChatPreview
GetChatPreview,
IterDialogs,
IterChatMembers
):
pass

View File

@ -39,10 +39,12 @@ class GetChatMembers(BaseClient):
limit: int = 200,
query: str = "",
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.
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:
chat_id (``int`` | ``str``):

View File

@ -16,19 +16,26 @@
# 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
import pyrogram
from pyrogram.api import functions, types
from pyrogram.api.errors import FloodWait
from ...ext import BaseClient
log = logging.getLogger(__name__)
class GetDialogs(BaseClient):
async def get_dialogs(self,
offset_date: int = 0,
limit: int = 100,
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.
For a more convenient way of getting a user's dialogs see :meth:`iter_dialogs`.
Args:
offset_date (``int``):
@ -50,18 +57,25 @@ class GetDialogs(BaseClient):
:class:`Error <pyrogram.Error>` in case of a Telegram RPC error.
"""
if pinned_only:
r = await self.send(functions.messages.GetPinnedDialogs())
else:
r = await self.send(
functions.messages.GetDialogs(
offset_date=offset_date,
offset_id=0,
offset_peer=types.InputPeerEmpty(),
limit=limit,
hash=0,
exclude_pinned=True
)
)
while True:
try:
if pinned_only:
r = await self.send(functions.messages.GetPinnedDialogs())
else:
r = await self.send(
functions.messages.GetDialogs(
offset_date=offset_date,
offset_id=0,
offset_peer=types.InputPeerEmpty(),
limit=limit,
hash=0,
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)

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,
chat_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.
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
@ -52,7 +52,7 @@ class KickChatMember(BaseClient):
considered to be banned forever. Defaults to 0 (ban forever).
Returns:
True on success.
On success, either True or a service :obj:`Message <pyrogram.Message>` will be returned (when applicable).
Raises:
: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.chats}
)
else:
return True

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/>.
from .close_poll import ClosePoll
from .delete_messages import DeleteMessages
from .download_media import DownloadMedia
from .edit_message_caption import EditMessageCaption
@ -25,6 +26,7 @@ from .edit_message_text import EditMessageText
from .forward_messages import ForwardMessages
from .get_history import GetHistory
from .get_messages import GetMessages
from .iter_history import IterHistory
from .retract_vote import RetractVote
from .send_animation import SendAnimation
from .send_audio import SendAudio
@ -69,7 +71,9 @@ class Messages(
SendVoice,
SendPoll,
VotePoll,
ClosePoll,
RetractVote,
DownloadMedia
DownloadMedia,
IterHistory
):
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_id: int = 0,
offset_date: int = 0,
reversed: bool = False):
"""Use this method to retrieve the history of a chat.
reverse: bool = False):
"""Use this method to retrieve a chunk of the history of a chat.
You can get up to 100 messages at once.
For a more convenient way of getting a chat history see :meth:`iter_history`.
Args:
chat_id (``int`` | ``str``):
@ -55,7 +56,7 @@ class GetHistory(BaseClient):
offset_date (``int``, *optional*):
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).
Returns:
@ -72,7 +73,7 @@ class GetHistory(BaseClient):
peer=await self.resolve_peer(chat_id),
offset_id=offset_id,
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,
max_id=0,
min_id=0,
@ -81,7 +82,7 @@ class GetHistory(BaseClient):
)
)
if reversed:
if reverse:
messages.messages.reverse()
return messages

View File

@ -16,19 +16,24 @@
# 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, Iterable
import pyrogram
from pyrogram.api import functions, types
from pyrogram.api.errors import FloodWait
from ...ext import BaseClient
log = logging.getLogger(__name__)
class GetMessages(BaseClient):
async def get_messages(self,
chat_id: Union[int, str],
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.
You can retrieve up to 200 messages at once.
@ -78,6 +83,15 @@ class GetMessages(BaseClient):
else:
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]

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):
self.peers_by_id = peers_by_id
def parse(self, text):
def parse(self, message: str):
entities = []
text = utils.add_surrogates(text)
message = utils.add_surrogates(str(message))
offset = 0
for match in self.HTML_RE.finditer(text):
for match in self.HTML_RE.finditer(message):
start = match.start() - offset
style, url, body = match.group(1, 3, 4)
@ -73,12 +73,12 @@ class HTML:
continue
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)
# TODO: OrderedDict to be removed in Python3.6
return OrderedDict([
("message", utils.remove_surrogates(text)),
("message", utils.remove_surrogates(message)),
("entities", entities)
])

View File

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

View File

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

View File

@ -23,3 +23,4 @@ from .inline_keyboard_markup import InlineKeyboardMarkup
from .keyboard_button import KeyboardButton
from .reply_keyboard_markup import ReplyKeyboardMarkup
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 (
KeyboardButtonUrl, KeyboardButtonCallback,
KeyboardButtonSwitchInline
KeyboardButtonSwitchInline, KeyboardButtonGame
)
from .callback_game import CallbackGame
from ..pyrogram_type import PyrogramType
@ -58,7 +59,8 @@ class InlineKeyboardButton(PyrogramType):
callback_data: bytes = None,
url: 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)
self.text = text
@ -66,7 +68,7 @@ class InlineKeyboardButton(PyrogramType):
self.callback_data = callback_data
self.switch_inline_query = switch_inline_query
self.switch_inline_query_current_chat = switch_inline_query_current_chat
# self.callback_game = callback_game
self.callback_game = callback_game
# self.pay = pay
@staticmethod
@ -95,6 +97,12 @@ class InlineKeyboardButton(PyrogramType):
switch_inline_query=o.query
)
if isinstance(o, KeyboardButtonGame):
return InlineKeyboardButton(
text=o.text,
callback_game=CallbackGame()
)
def write(self):
if self.callback_data:
return KeyboardButtonCallback(self.text, self.callback_data)
@ -107,3 +115,6 @@ class InlineKeyboardButton(PyrogramType):
if self.switch_inline_query_current_chat:
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):
"""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>`.
Args:

View File

@ -34,3 +34,4 @@ from .venue import Venue
from .video import Video
from .video_note import VideoNote
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*):
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*):
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
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*):
Channel post views.
@ -255,6 +262,7 @@ class Message(PyrogramType, Update):
photo: "pyrogram.Photo" = None,
sticker: "pyrogram.Sticker" = None,
animation: "pyrogram.Animation" = None,
game: "pyrogram.Game" = None,
video: "pyrogram.Video" = None,
voice: "pyrogram.Voice" = None,
video_note: "pyrogram.VideoNote" = None,
@ -275,6 +283,7 @@ class Message(PyrogramType, Update):
migrate_to_chat_id: int = None,
migrate_from_chat_id: int = None,
pinned_message: "Message" = None,
game_score: int = None,
views: int = None,
via_bot: User = None,
outgoing: bool = None,
@ -311,6 +320,7 @@ class Message(PyrogramType, Update):
self.photo = photo
self.sticker = sticker
self.animation = animation
self.game = game
self.video = video
self.voice = voice
self.video_note = video_note
@ -331,6 +341,7 @@ class Message(PyrogramType, Update):
self.migrate_to_chat_id = migrate_to_chat_id
self.migrate_from_chat_id = migrate_from_chat_id
self.pinned_message = pinned_message
self.game_score = game_score
self.views = views
self.via_bot = via_bot
self.outgoing = outgoing
@ -407,6 +418,19 @@ class Message(PyrogramType, Update):
except MessageIdsEmpty:
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
if isinstance(message, types.Message):
@ -435,6 +459,7 @@ class Message(PyrogramType, Update):
location = None
contact = None
venue = None
game = None
audio = None
voice = None
animation = None
@ -456,6 +481,8 @@ class Message(PyrogramType, Update):
contact = Contact._parse(client, media)
elif isinstance(media, types.MessageMediaVenue):
venue = pyrogram.Venue._parse(client, media)
elif isinstance(media, types.MessageMediaGame):
game = pyrogram.Game._parse(client, message)
elif isinstance(media, types.MessageMediaDocument):
doc = media.document
@ -543,6 +570,7 @@ class Message(PyrogramType, Update):
audio=audio,
voice=voice,
animation=animation,
game=game,
video=video,
video_note=video_note,
sticker=sticker,

View File

@ -52,14 +52,41 @@ class Messages(PyrogramType, Update):
users = {i.id: i for i in messages.users}
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
parsed_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(
total_count=getattr(messages, "count", len(messages.messages)),
total_count=total_count,
messages=parsed_messages,
client=client
)