* Pure API changes

* Address review

* set Bot.commands on successfull call of set_my_commands

* Get started on tests

* More tests!

* More Coverage!

* Reset changes in utils.request

* Filters.dice, Filters.dice.text

* more coverage

* Address review

* Address review

* Test stop_poll with reply_markup

* Test stop_poll also without reply_markup

* Rephrase note on 'dice'

* Fix grammar in note on Filters.dice

* update api version readme

* address review
This commit is contained in:
Bibo-Joshi 2020-04-10 19:22:45 +02:00 committed by GitHub
parent f379f54d5a
commit d63e710784
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
19 changed files with 686 additions and 47 deletions

View file

@ -93,7 +93,7 @@ make the development of bots easy and straightforward. These classes are contain
Telegram API support
====================
All types and methods of the Telegram Bot API **4.6** are supported.
All types and methods of the Telegram Bot API **4.7** are supported.
==========
Installing

View file

@ -0,0 +1,6 @@
telegram.BotCommand
===================
.. autoclass:: telegram.BotCommand
:members:
:show-inheritance:

View file

@ -0,0 +1,6 @@
telegram.Dice
=============
.. autoclass:: telegram.Dice
:members:
:show-inheritance:

View file

@ -9,6 +9,7 @@ telegram package
telegram.animation
telegram.audio
telegram.bot
telegram.botcommand
telegram.callbackquery
telegram.chat
telegram.chataction
@ -17,6 +18,7 @@ telegram package
telegram.chatphoto
telegram.constants
telegram.contact
telegram.dice
telegram.document
telegram.error
telegram.file

View file

@ -19,6 +19,7 @@
"""A library that provides a Python interface to the Telegram Bot API"""
from .base import TelegramObject
from .botcommand import BotCommand
from .user import User
from .files.chatphoto import ChatPhoto
from .chat import Chat
@ -36,6 +37,7 @@ from .files.location import Location
from .files.venue import Venue
from .files.videonote import VideoNote
from .chataction import ChatAction
from .dice import Dice
from .userprofilephotos import UserProfilePhotos
from .keyboardbutton import KeyboardButton
from .keyboardbuttonpolltype import KeyboardButtonPollType
@ -157,5 +159,6 @@ __all__ = [
'InputMediaAudio', 'InputMediaDocument', 'TelegramDecryptionError',
'PassportElementErrorSelfie', 'PassportElementErrorTranslationFile',
'PassportElementErrorTranslationFiles', 'PassportElementErrorUnspecified', 'Poll',
'PollOption', 'PollAnswer', 'LoginUrl', 'KeyboardButton', 'KeyboardButtonPollType',
'PollOption', 'PollAnswer', 'LoginUrl', 'KeyboardButton', 'KeyboardButtonPollType', 'Dice',
'BotCommand'
]

View file

@ -39,7 +39,7 @@ from future.utils import string_types
from telegram import (User, Message, Update, Chat, ChatMember, UserProfilePhotos, File,
ReplyMarkup, TelegramObject, WebhookInfo, GameHighScore, StickerSet,
PhotoSize, Audio, Document, Sticker, Video, Animation, Voice, VideoNote,
Location, Venue, Contact, InputFile, Poll)
Location, Venue, Contact, InputFile, Poll, BotCommand)
from telegram.error import InvalidToken, TelegramError
from telegram.utils.helpers import to_timestamp, DEFAULT_NONE
from telegram.utils.request import Request
@ -53,6 +53,9 @@ def info(func):
if not self.bot:
self.get_me()
if self._commands is None:
self.get_my_commands()
result = func(self, *args, **kwargs)
return result
@ -141,6 +144,7 @@ class Bot(TelegramObject):
self.base_url = str(base_url) + str(self.token)
self.base_file_url = str(base_file_url) + str(self.token)
self.bot = None
self._commands = None
self._request = request or Request()
self.logger = logging.getLogger(__name__)
@ -159,7 +163,7 @@ class Bot(TelegramObject):
if reply_markup is not None:
if isinstance(reply_markup, ReplyMarkup):
data['reply_markup'] = reply_markup.to_json()
data['reply_markup'] = reply_markup.to_dict()
else:
data['reply_markup'] = reply_markup
@ -251,6 +255,13 @@ class Bot(TelegramObject):
return self.bot.supports_inline_queries
@property
@info
def commands(self):
"""List[:class:`BotCommand`]: Bot's commands."""
return self._commands
@property
def name(self):
""":obj:`str`: Bot's @username."""
@ -344,6 +355,8 @@ class Bot(TelegramObject):
limitations:
- A message can only be deleted if it was sent less than 48 hours ago.
- A dice message in a private chat can only be deleted if it was sent more than 24
hours ago.
- Bots can delete outgoing messages in private chats, groups, and supergroups.
- Bots can delete incoming messages in private chats.
- Bots granted can_post_messages permissions can delete outgoing messages in channels.
@ -3308,15 +3321,23 @@ class Bot(TelegramObject):
return File.de_json(result, self)
@log
def create_new_sticker_set(self, user_id, name, title, png_sticker, emojis,
contains_masks=None, mask_position=None, timeout=20, **kwargs):
def create_new_sticker_set(self, user_id, name, title, emojis, png_sticker=None,
contains_masks=None, mask_position=None, timeout=20,
tgs_sticker=None, **kwargs):
"""Use this method to create new sticker set owned by a user.
The bot will be able to edit the created sticker set.
You must use exactly one of the fields :attr:`png_sticker` or :attr:`tgs_sticker`.
Warning:
As of API 4.7 ``png_sticker`` is an optional argument and therefore the order of the
arguments had to be changed. Use keyword arguments to make sure that the arguments are
passed correctly.
Note:
The png_sticker argument can be either a file_id, an URL or a file from disk
``open(filename, 'rb')``
The png_sticker and tgs_sticker argument can be either a file_id, an URL or a file from
disk ``open(filename, 'rb')``
Args:
user_id (:obj:`int`): User identifier of created sticker set owner.
@ -3326,12 +3347,16 @@ class Bot(TelegramObject):
must end in "_by_<bot username>". <bot_username> is case insensitive.
1-64 characters.
title (:obj:`str`): Sticker set title, 1-64 characters.
png_sticker (:obj:`str` | `filelike object`): Png image with the sticker, must be up
to 512 kilobytes in size, dimensions must not exceed 512px,
png_sticker (:obj:`str` | `filelike object`, optional): Png image with the sticker,
must be up to 512 kilobytes in size, dimensions must not exceed 512px,
and either width or height must be exactly 512px. Pass a file_id as a String to
send a file that already exists on the Telegram servers, pass an HTTP URL as a
String for Telegram to get a file from the Internet, or upload a new one
using multipart/form-data.
tgs_sticker (:obj:`str` | `filelike object`, optional): TGS animation with the sticker,
uploaded using multipart/form-data. See
https://core.telegram.org/animated_stickers#technical-requirements for technical
requirements
emojis (:obj:`str`): One or more emoji corresponding to the sticker.
contains_masks (:obj:`bool`, optional): Pass True, if a set of mask stickers should be
created.
@ -3354,13 +3379,19 @@ class Bot(TelegramObject):
if InputFile.is_file(png_sticker):
png_sticker = InputFile(png_sticker)
data = {'user_id': user_id, 'name': name, 'title': title, 'png_sticker': png_sticker,
'emojis': emojis}
if InputFile.is_file(tgs_sticker):
tgs_sticker = InputFile(tgs_sticker)
data = {'user_id': user_id, 'name': name, 'title': title, 'emojis': emojis}
if png_sticker is not None:
data['png_sticker'] = png_sticker
if tgs_sticker is not None:
data['tgs_sticker'] = tgs_sticker
if contains_masks is not None:
data['contains_masks'] = contains_masks
if mask_position is not None:
data['mask_position'] = mask_position
data['mask_position'] = mask_position.to_json()
data.update(kwargs)
result = self._request.post(url, data, timeout=timeout)
@ -3368,23 +3399,35 @@ class Bot(TelegramObject):
return result
@log
def add_sticker_to_set(self, user_id, name, png_sticker, emojis, mask_position=None,
timeout=20, **kwargs):
"""Use this method to add a new sticker to a set created by the bot.
def add_sticker_to_set(self, user_id, name, emojis, png_sticker=None, mask_position=None,
timeout=20, tgs_sticker=None, **kwargs):
"""Use this method to add a new sticker to a set created by the bot. You must use exactly
one of the fields png_sticker or tgs_sticker. Animated stickers can be added to animated
sticker sets and only to them. Animated sticker sets can have up to 50 stickers. Static
sticker sets can have up to 120 stickers.
Warning:
As of API 4.7 ``png_sticker`` is an optional argument and therefore the order of the
arguments had to be changed. Use keyword arguments to make sure that the arguments are
passed correctly.
Note:
The png_sticker argument can be either a file_id, an URL or a file from disk
``open(filename, 'rb')``
The png_sticker and tgs_sticker argument can be either a file_id, an URL or a file from
disk ``open(filename, 'rb')``
Args:
user_id (:obj:`int`): User identifier of created sticker set owner.
name (:obj:`str`): Sticker set name.
png_sticker (:obj:`str` | `filelike object`): Png image with the sticker, must be up
to 512 kilobytes in size, dimensions must not exceed 512px,
png_sticker (:obj:`str` | `filelike object`, optional): Png image with the sticker,
must be up to 512 kilobytes in size, dimensions must not exceed 512px,
and either width or height must be exactly 512px. Pass a file_id as a String to
send a file that already exists on the Telegram servers, pass an HTTP URL as a
String for Telegram to get a file from the Internet, or upload a new one
using multipart/form-data.
tgs_sticker (:obj:`str` | `filelike object`, optional): TGS animation with the sticker,
uploaded using multipart/form-data. See
https://core.telegram.org/animated_stickers#technical-requirements for technical
requirements
emojis (:obj:`str`): One or more emoji corresponding to the sticker.
mask_position (:class:`telegram.MaskPosition`, optional): Position where the mask
should beplaced on faces.
@ -3405,10 +3448,17 @@ class Bot(TelegramObject):
if InputFile.is_file(png_sticker):
png_sticker = InputFile(png_sticker)
data = {'user_id': user_id, 'name': name, 'png_sticker': png_sticker, 'emojis': emojis}
if InputFile.is_file(tgs_sticker):
tgs_sticker = InputFile(tgs_sticker)
data = {'user_id': user_id, 'name': name, 'emojis': emojis}
if png_sticker is not None:
data['png_sticker'] = png_sticker
if tgs_sticker is not None:
data['tgs_sticker'] = tgs_sticker
if mask_position is not None:
data['mask_position'] = mask_position
data['mask_position'] = mask_position.to_json()
data.update(kwargs)
result = self._request.post(url, data, timeout=timeout)
@ -3470,6 +3520,48 @@ class Bot(TelegramObject):
return result
@log
def set_sticker_set_thumb(self, name, user_id, thumb=None, timeout=None, **kwargs):
"""Use this method to set the thumbnail of a sticker set. Animated thumbnails can be set
for animated sticker sets only.
Note:
The thumb can be either a file_id, an URL or a file from disk ``open(filename, 'rb')``
Args:
name (:obj:`str`): Sticker set name
user_id (:obj:`int`): User identifier of created sticker set owner.
thumb (:obj:`str` | `filelike object`, optional): A PNG image with the thumbnail, must
be up to 128 kilobytes in size and have width and height exactly 100px, or a TGS
animation with the thumbnail up to 32 kilobytes in size; see
https://core.telegram.org/animated_stickers#technical-requirements for animated sticker
technical requirements. Pass a file_id as a String to send a file that already exists
on the Telegram servers, pass an HTTP URL as a String for Telegram to get a file from
the Internet, or upload a new one using multipart/form-data.
timeout (:obj:`int` | :obj:`float`, optional): If this value is specified, use it as
the read timeout from the server (instead of the one specified during
creation of the connection pool).
**kwargs (:obj:`dict`): Arbitrary keyword arguments.
Returns:
:obj:`bool`: On success, ``True`` is returned.
Raises:
:class:`telegram.TelegramError`
"""
url = '{}/setStickerSetThumb'.format(self.base_url)
if InputFile.is_file(thumb):
thumb = InputFile(thumb)
data = {'name': name, 'user_id': user_id, 'thumb': thumb}
data.update(kwargs)
result = self._request.post(url, data, timeout=timeout)
return result
@log
def set_passport_data_errors(self, user_id, errors, timeout=None, **kwargs):
"""
@ -3621,7 +3713,7 @@ class Bot(TelegramObject):
if reply_markup:
if isinstance(reply_markup, ReplyMarkup):
data['reply_markup'] = reply_markup.to_json()
data['reply_markup'] = reply_markup.to_dict()
else:
data['reply_markup'] = reply_markup
@ -3629,6 +3721,111 @@ class Bot(TelegramObject):
return Poll.de_json(result, self)
@log
def send_dice(self,
chat_id,
disable_notification=None,
reply_to_message_id=None,
reply_markup=None,
timeout=None,
**kwargs):
"""
Use this method to send a dice, which will have a random value from 1 to 6. On success, the
sent Message is returned.
Args:
chat_id (:obj:`int` | :obj:`str`): Unique identifier for the target private chat.
disable_notification (:obj:`bool`, optional): Sends the message silently. Users will
receive a notification with no sound.
reply_to_message_id (:obj:`int`, optional): If the message is a reply, ID of the
original message.
reply_markup (:class:`telegram.ReplyMarkup`, optional): Additional interface options. A
JSON-serialized object for an inline keyboard, custom reply keyboard, instructions
to remove reply keyboard or to force a reply from the user.
timeout (:obj:`int` | :obj:`float`, optional): If this value is specified, use it as
the read timeout from the server (instead of the one specified during creation of
the connection pool).
**kwargs (:obj:`dict`): Arbitrary keyword arguments.
Returns:
:class:`telegram.Message`: On success, the sent Message is returned.
Raises:
:class:`telegram.TelegramError`
"""
url = '{0}/sendDice'.format(self.base_url)
data = {
'chat_id': chat_id,
}
return self._message(url, data, timeout=timeout, disable_notification=disable_notification,
reply_to_message_id=reply_to_message_id, reply_markup=reply_markup,
**kwargs)
@log
def get_my_commands(self, timeout=None, **kwargs):
"""
Use this method to get the current list of the bot's commands.
Args:
timeout (:obj:`int` | :obj:`float`, optional): If this value is specified, use it as
the read timeout from the server (instead of the one specified during creation of
the connection pool).
**kwargs (:obj:`dict`): Arbitrary keyword arguments.
Returns:
List[:class:`telegram.BotCommand]`: On success, the commands set for the bot
Raises:
:class:`telegram.TelegramError`
"""
url = '{0}/getMyCommands'.format(self.base_url)
result = self._request.get(url, timeout=timeout)
self._commands = [BotCommand.de_json(c, self) for c in result]
return self._commands
@log
def set_my_commands(self, commands, timeout=None, **kwargs):
"""
Use this method to change the list of the bot's commands.
Args:
commands (List[:class:`BotCommand` | (:obj:`str`, :obj:`str`)]): A JSON-serialized list
of bot commands to be set as the list of the bot's commands. At most 100 commands
can be specified.
timeout (:obj:`int` | :obj:`float`, optional): If this value is specified, use it as
the read timeout from the server (instead of the one specified during creation of
the connection pool).
**kwargs (:obj:`dict`): Arbitrary keyword arguments.
Returns:
:obj:`True`: On success
Raises:
:class:`telegram.TelegramError`
"""
url = '{0}/setMyCommands'.format(self.base_url)
cmds = [c if isinstance(c, BotCommand) else BotCommand(c[0], c[1]) for c in commands]
data = {'commands': [c.to_dict() for c in cmds]}
data.update(kwargs)
result = self._request.post(url, data, timeout=timeout)
# Set commands. No need to check for outcome.
# If request failed, we won't come this far
self._commands = commands
return result
def to_dict(self):
data = {'id': self.id, 'username': self.username, 'first_name': self.first_name}
@ -3768,9 +3965,17 @@ class Bot(TelegramObject):
"""Alias for :attr:`set_sticker_position_in_set`"""
deleteStickerFromSet = delete_sticker_from_set
"""Alias for :attr:`delete_sticker_from_set`"""
setStickerSetThumb = set_sticker_set_thumb
"""Alias for :attr:`set_sticker_set_thumb`"""
setPassportDataErrors = set_passport_data_errors
"""Alias for :attr:`set_passport_data_errors`"""
sendPoll = send_poll
"""Alias for :attr:`send_poll`"""
stopPoll = stop_poll
"""Alias for :attr:`stop_poll`"""
sendDice = send_dice
"""Alias for :attr:`send_dice`"""
getMyCommands = get_my_commands
"""Alias for :attr:`get_my_commands`"""
setMyCommands = set_my_commands
"""Alias for :attr:`set_my_commands`"""

46
telegram/botcommand.py Normal file
View file

@ -0,0 +1,46 @@
#!/usr/bin/env python
# pylint: disable=R0903
#
# A library that provides a Python interface to the Telegram Bot API
# Copyright (C) 2015-2020
# Leandro Toledo de Souza <devs@python-telegram-bot.org>
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Lesser Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Lesser Public License for more details.
#
# You should have received a copy of the GNU Lesser Public License
# along with this program. If not, see [http://www.gnu.org/licenses/].
"""This module contains an object that represents a Telegram Bot Command."""
from telegram import TelegramObject
class BotCommand(TelegramObject):
"""
This object represents a bot command.
Attributes:
command (:obj:`str`): Text of the command.
description (:obj:`str`): Description of the command.
Args:
command (:obj:`str`): Text of the command, 1-32 characters. Can contain only lowercase
English letters, digits and underscores.
description (:obj:`str`): Description of the command, 3-256 characters.
"""
def __init__(self, command, description, **kwargs):
self.command = command
self.description = description
@classmethod
def de_json(cls, data, bot):
if not data:
return None
return cls(**data)

43
telegram/dice.py Normal file
View file

@ -0,0 +1,43 @@
#!/usr/bin/env python
# pylint: disable=R0903
#
# A library that provides a Python interface to the Telegram Bot API
# Copyright (C) 2015-2020
# Leandro Toledo de Souza <devs@python-telegram-bot.org>
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Lesser Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Lesser Public License for more details.
#
# You should have received a copy of the GNU Lesser Public License
# along with this program. If not, see [http://www.gnu.org/licenses/].
"""This module contains an object that represents a Telegram Dice."""
from telegram import TelegramObject
class Dice(TelegramObject):
"""
This object represents a dice with random value from 1 to 6. (The singular form of "dice" is
"die". However, PTB mimics the Telegram API, which uses the term "dice".)
Attributes:
value (:obj:`int`): Value of the dice.
Args:
value (:obj:`int`): Value of the dice, 1-6.
"""
def __init__(self, value, **kwargs):
self.value = value
@classmethod
def de_json(cls, data, bot):
if not data:
return None
return cls(**data)

View file

@ -272,6 +272,10 @@ class Filters(object):
...
MessageHandler(Filters.text(buttons), callback_method)
Note:
Dice messages don't have text. If you want to filter either text or dice messages, use
``Filters.text | Filters.dice``.
Args:
update (List[:obj:`str`] | Tuple[:obj:`str`], optional): Which messages to allow. Only
exact matches are allowed. If not specified, will allow any text message.
@ -427,7 +431,7 @@ class Filters(object):
send media with wrong types that don't fit to this handler.
Example:
Filters.documents.category('audio/') returnes `True` for all types
Filters.documents.category('audio/') returns `True` for all types
of audio sent as file, for example 'audio/mpeg' or 'audio/x-wav'
"""
@ -957,6 +961,48 @@ officedocument.wordprocessingml.document")``-
poll = _Poll()
"""Messages that contain a :class:`telegram.Poll`."""
class _Dice(BaseFilter):
name = 'Filters.dice'
class _DiceValues(BaseFilter):
def __init__(self, values):
self.values = [values] if isinstance(values, int) else values
self.name = 'Filters.dice({})'.format(values)
def filter(self, message):
return bool(message.dice and message.dice.value in self.values)
def __call__(self, update):
if isinstance(update, Update):
return self.filter(update.effective_message)
else:
return self._DiceValues(update)
def filter(self, message):
return bool(message.dice)
dice = _Dice()
"""Dice Messages. If an integer or a list of integers is passed, it filters messages to only
allow those whose dice value is appearing in the given list.
Examples:
To allow any dice message, simply use
``MessageHandler(Filters.dice, callback_method)``.
To allow only dice with value 6, use
``MessageHandler(Filters.dice(6), callback_method)``.
To allow only dice with value 5 `or` 6, use
``MessageHandler(Filters.dice([5, 6]), callback_method)``.
Args:
update (:obj:`int` | List[:obj:`int`], optional): Which values to allow. If not
specified, will allow any dice message.
Note:
Dice messages don't have text. If you want to filter either text or dice messages, use
``Filters.text | Filters.dice``.
"""
class language(BaseFilter):
"""Filters messages to only allow those which are from users with a certain language code.

View file

@ -138,6 +138,8 @@ class StickerSet(TelegramObject):
is_animated (:obj:`bool`): True, if the sticker set contains animated stickers.
contains_masks (:obj:`bool`): True, if the sticker set contains masks.
stickers (List[:class:`telegram.Sticker`]): List of all set stickers.
thumb (:class:`telegram.PhotoSize`): Optional. Sticker set thumbnail in the .WEBP or .TGS
format
Args:
name (:obj:`str`): Sticker set name.
@ -145,15 +147,20 @@ class StickerSet(TelegramObject):
is_animated (:obj:`bool`): True, if the sticker set contains animated stickers.
contains_masks (:obj:`bool`): True, if the sticker set contains masks.
stickers (List[:class:`telegram.Sticker`]): List of all set stickers.
thumb (:class:`telegram.PhotoSize`, optional): Sticker set thumbnail in the .WEBP or .TGS
format
"""
def __init__(self, name, title, is_animated, contains_masks, stickers, bot=None, **kwargs):
def __init__(self, name, title, is_animated, contains_masks, stickers, bot=None, thumb=None,
**kwargs):
self.name = name
self.title = title
self.is_animated = is_animated
self.contains_masks = contains_masks
self.stickers = stickers
# Optionals
self.thumb = thumb
self._id_attrs = (self.name,)
@ -164,6 +171,7 @@ class StickerSet(TelegramObject):
data = super(StickerSet, StickerSet).de_json(data, bot)
data['thumb'] = PhotoSize.de_json(data.get('thumb'), bot)
data['stickers'] = Sticker.de_list(data.get('stickers'), bot)
return StickerSet(bot=bot, **data)

View file

@ -23,7 +23,7 @@ from html import escape
from telegram import (Animation, Audio, Contact, Document, Chat, Location, PhotoSize, Sticker,
TelegramObject, User, Video, Voice, Venue, MessageEntity, Game, Invoice,
SuccessfulPayment, VideoNote, PassportData, Poll, InlineKeyboardMarkup)
SuccessfulPayment, VideoNote, PassportData, Poll, InlineKeyboardMarkup, Dice)
from telegram import ParseMode
from telegram.utils.helpers import escape_markdown, to_timestamp, from_timestamp
@ -106,6 +106,7 @@ class Message(TelegramObject):
passport_data (:class:`telegram.PassportData`): Optional. Telegram Passport data.
poll (:class:`telegram.Poll`): Optional. Message is a native poll,
information about the poll.
dice (:class:`telegram.Dice`): Optional. Message is a dice.
reply_markup (:class:`telegram.InlineKeyboardMarkup`): Optional. Inline keyboard attached
to the message.
bot (:class:`telegram.Bot`): Optional. The Bot to use for instance methods.
@ -199,7 +200,7 @@ class Message(TelegramObject):
smaller than 52 bits, so a signed 64 bit integer or double-precision float type are
safe for storing this identifier.
pinned_message (:class:`telegram.message`, optional): Specified message was pinned. Note
that the Message object in this field will not contain further attr:`reply_to_message`
that the Message object in this field will not contain further :attr:`reply_to_message`
fields even if it is itself a reply.
invoice (:class:`telegram.Invoice`, optional): Message is an invoice for a payment,
information about the invoice.
@ -214,6 +215,7 @@ class Message(TelegramObject):
passport_data (:class:`telegram.PassportData`, optional): Telegram Passport data.
poll (:class:`telegram.Poll`, optional): Message is a native poll,
information about the poll.
dice (:class:`telegram.Dice`, optional): Message is a dice with random value from 1 to 6.
reply_markup (:class:`telegram.InlineKeyboardMarkup`, optional): Inline keyboard attached
to the message. login_url buttons are represented as ordinary url buttons.
default_quote (:obj:`bool`, optional): Default setting for the `quote` parameter of the
@ -229,7 +231,7 @@ class Message(TelegramObject):
MESSAGE_TYPES = ['text', 'new_chat_members', 'left_chat_member', 'new_chat_title',
'new_chat_photo', 'delete_chat_photo', 'group_chat_created',
'supergroup_chat_created', 'channel_chat_created', 'migrate_to_chat_id',
'migrate_from_chat_id', 'pinned_message',
'migrate_from_chat_id', 'pinned_message', 'poll', 'dice',
'passport_data'] + ATTACHMENT_TYPES
def __init__(self,
@ -282,6 +284,7 @@ class Message(TelegramObject):
reply_markup=None,
bot=None,
default_quote=None,
dice=None,
**kwargs):
# Required
self.message_id = int(message_id)
@ -331,6 +334,7 @@ class Message(TelegramObject):
self.animation = animation
self.passport_data = passport_data
self.poll = poll
self.dice = dice
self.reply_markup = reply_markup
self.bot = bot
self.default_quote = default_quote
@ -404,6 +408,7 @@ class Message(TelegramObject):
data['successful_payment'] = SuccessfulPayment.de_json(data.get('successful_payment'), bot)
data['passport_data'] = PassportData.de_json(data.get('passport_data'), bot)
data['poll'] = Poll.de_json(data.get('poll'), bot)
data['dice'] = Dice.de_json(data.get('dice'), bot)
data['reply_markup'] = InlineKeyboardMarkup.de_json(data.get('reply_markup'), bot)
return cls(bot=bot, **data)
@ -808,6 +813,23 @@ class Message(TelegramObject):
self._quote(kwargs)
return self.bot.send_poll(self.chat_id, *args, **kwargs)
def reply_dice(self, *args, **kwargs):
"""Shortcut for::
bot.send_dice(update.message.chat_id, *args, **kwargs)
Keyword Args:
quote (:obj:`bool`, optional): If set to ``True``, the dice is sent as an actual reply
to this message. If ``reply_to_message_id`` is passed in ``kwargs``, this parameter
will be ignored. Default: ``True`` in group chats and ``False`` in private chats.
Returns:
:class:`telegram.Message`: On success, instance representing the message posted.
"""
self._quote(kwargs)
return self.bot.send_dice(self.chat_id, *args, **kwargs)
def forward(self, chat_id, *args, **kwargs):
"""Shortcut for::

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

View file

@ -26,7 +26,7 @@ from future.utils import string_types
from telegram import (Bot, Update, ChatAction, TelegramError, User, InlineKeyboardMarkup,
InlineKeyboardButton, InlineQueryResultArticle, InputTextMessageContent,
ShippingOption, LabeledPrice, ChatPermissions, Poll,
ShippingOption, LabeledPrice, ChatPermissions, Poll, BotCommand,
InlineQueryResultDocument)
from telegram.error import BadRequest, InvalidToken, NetworkError, RetryAfter
from telegram.utils.helpers import from_timestamp, escape_markdown
@ -80,6 +80,7 @@ class TestBot(object):
@pytest.mark.timeout(10)
def test_get_me_and_properties(self, bot):
get_me_bot = bot.get_me()
commands = bot.get_my_commands()
assert isinstance(get_me_bot, User)
assert get_me_bot.id == bot.id
@ -91,6 +92,7 @@ class TestBot(object):
assert get_me_bot.can_read_all_group_messages == bot.can_read_all_group_messages
assert get_me_bot.supports_inline_queries == bot.supports_inline_queries
assert 'https://t.me/{}'.format(get_me_bot.username) == bot.link
assert commands == bot.commands
@flaky(3, 1)
@pytest.mark.timeout(10)
@ -174,7 +176,13 @@ class TestBot(object):
@flaky(3, 1)
@pytest.mark.timeout(10)
def test_send_and_stop_poll(self, bot, super_group_id):
@pytest.mark.parametrize('reply_markup', [
None,
InlineKeyboardMarkup.from_button(InlineKeyboardButton(text='text', callback_data='data')),
InlineKeyboardMarkup.from_button(
InlineKeyboardButton(text='text', callback_data='data')).to_dict()
])
def test_send_and_stop_poll(self, bot, super_group_id, reply_markup):
question = 'Is this a test?'
answers = ['Yes', 'No', 'Maybe']
message = bot.send_poll(chat_id=super_group_id, question=question, options=answers,
@ -190,7 +198,10 @@ class TestBot(object):
assert not message.poll.is_closed
assert message.poll.type == Poll.REGULAR
poll = bot.stop_poll(chat_id=super_group_id, message_id=message.message_id, timeout=60)
# Since only the poll and not the complete message is returned, we can't check that the
# reply_markup is correct. So we just test that sending doesn't give an error.
poll = bot.stop_poll(chat_id=super_group_id, message_id=message.message_id,
reply_markup=reply_markup, timeout=60)
assert isinstance(poll, Poll)
assert poll.is_closed
assert poll.options[0].text == answers[0]
@ -208,6 +219,13 @@ class TestBot(object):
assert message_quiz.poll.type == Poll.QUIZ
assert message_quiz.poll.is_closed
@flaky(3, 1)
@pytest.mark.timeout(10)
def test_send_dice(self, bot, chat_id):
message = bot.send_dice(chat_id)
assert message.dice
@flaky(3, 1)
@pytest.mark.timeout(10)
def test_send_chat_action(self, bot, chat_id):
@ -904,3 +922,41 @@ class TestBot(object):
def test_send_message_default_quote(self, default_bot, chat_id):
message = default_bot.send_message(chat_id, 'test')
assert message.default_quote is True
@flaky(3, 1)
@pytest.mark.timeout(10)
def test_set_and_get_my_commands(self, bot):
commands = [
BotCommand('cmd1', 'descr1'),
BotCommand('cmd2', 'descr2'),
]
bot.set_my_commands([])
assert bot.get_my_commands() == []
assert bot.commands == []
assert bot.set_my_commands(commands)
for bc in [bot.get_my_commands(), bot.commands]:
assert len(bc) == 2
assert bc[0].command == 'cmd1'
assert bc[0].description == 'descr1'
assert bc[1].command == 'cmd2'
assert bc[1].description == 'descr2'
@flaky(3, 1)
@pytest.mark.timeout(10)
def test_set_and_get_my_commands_strings(self, bot):
commands = [
['cmd1', 'descr1'],
['cmd2', 'descr2'],
]
bot.set_my_commands([])
assert bot.get_my_commands() == []
assert bot.commands == []
assert bot.set_my_commands(commands)
for bc in [bot.get_my_commands(), bot.commands]:
assert len(bc) == 2
assert bc[0].command == 'cmd1'
assert bc[0].description == 'descr1'
assert bc[1].command == 'cmd2'
assert bc[1].description == 'descr2'

48
tests/test_botcommand.py Normal file
View file

@ -0,0 +1,48 @@
#!/usr/bin/env python
#
# A library that provides a Python interface to the Telegram Bot API
# Copyright (C) 2015-2020
# Leandro Toledo de Souza <devs@python-telegram-bot.org>
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Lesser Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Lesser Public License for more details.
#
# You should have received a copy of the GNU Lesser Public License
# along with this program. If not, see [http://www.gnu.org/licenses/].
import pytest
from telegram import BotCommand
@pytest.fixture(scope="class")
def bot_command():
return BotCommand(command='start', description='A command')
class TestBotCommand(object):
command = 'start'
description = 'A command'
def test_de_json(self, bot):
json_dict = {'command': self.command, 'description': self.description}
bot_command = BotCommand.de_json(json_dict, bot)
assert bot_command.command == self.command
assert bot_command.description == self.description
assert BotCommand.de_json(None, bot) is None
def test_to_dict(self, bot_command):
bot_command_dict = bot_command.to_dict()
assert isinstance(bot_command_dict, dict)
assert bot_command_dict['command'] == bot_command.command
assert bot_command_dict['description'] == bot_command.description

44
tests/test_dice.py Normal file
View file

@ -0,0 +1,44 @@
#!/usr/bin/env python
#
# A library that provides a Python interface to the Telegram Bot API
# Copyright (C) 2015-2020
# Leandro Toledo de Souza <devs@python-telegram-bot.org>
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Lesser Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Lesser Public License for more details.
#
# You should have received a copy of the GNU Lesser Public License
# along with this program. If not, see [http://www.gnu.org/licenses/].
import pytest
from telegram import Dice
@pytest.fixture(scope="class")
def dice():
return Dice(value=5)
class TestDice(object):
value = 4
def test_de_json(self, bot):
json_dict = {'value': self.value}
dice = Dice.de_json(json_dict, bot)
assert dice.value == self.value
assert Dice.de_json(None, bot) is None
def test_to_dict(self, dice):
dice_dict = dice.to_dict()
assert isinstance(dice_dict, dict)
assert dice_dict['value'] == dice.value

View file

@ -20,7 +20,7 @@ import datetime
import pytest
from telegram import Message, User, Chat, MessageEntity, Document, Update
from telegram import Message, User, Chat, MessageEntity, Document, Update, Dice
from telegram.ext import Filters, BaseFilter
import re
@ -622,6 +622,22 @@ class TestFilters(object):
update.message.poll = 'test'
assert Filters.poll(update)
def test_filters_dice(self, update):
update.message.dice = Dice(4)
assert Filters.dice(update)
update.message.dice = None
assert not Filters.dice(update)
def test_filters_dice_iterable(self, update):
update.message.dice = None
assert not Filters.dice(5)(update)
update.message.dice = Dice(5)
assert Filters.dice(5)(update)
assert Filters.dice({5, 6})(update)
assert not Filters.dice(1)(update)
assert not Filters.dice([2, 3])(update)
def test_language_filter_single(self, update):
update.message.from_user.language_code = 'en_US'
assert (Filters.language('en_US'))(update)

View file

@ -22,7 +22,7 @@ import pytest
from telegram import (Update, Message, User, MessageEntity, Chat, Audio, Document, Animation,
Game, PhotoSize, Sticker, Video, Voice, VideoNote, Contact, Location, Venue,
Invoice, SuccessfulPayment, PassportData, ParseMode, Poll, PollOption)
Invoice, SuccessfulPayment, PassportData, ParseMode, Poll, PollOption, Dice)
from tests.test_passport import RAW_PASSPORT_DATA
@ -97,7 +97,8 @@ def message(bot):
'text': 'start', 'url': 'http://google.com'}, {
'text': 'next', 'callback_data': 'abcd'}],
[{'text': 'Cancel', 'callback_data': 'Cancel'}]]}},
{'quote': True}
{'quote': True},
{'dice': Dice(4)}
],
ids=['forwarded_user', 'forwarded_channel', 'reply', 'edited', 'text',
'caption_entities', 'audio', 'document', 'animation', 'game', 'photo',
@ -107,7 +108,7 @@ def message(bot):
'migrated_from', 'pinned', 'invoice', 'successful_payment',
'connected_website', 'forward_signature', 'author_signature',
'photo_from_media_group', 'passport_data', 'poll', 'reply_markup',
'default_quote'])
'default_quote', 'dice'])
def message_params(bot, request):
return Message(message_id=TestMessage.id_,
from_user=TestMessage.from_user,
@ -702,7 +703,7 @@ class TestMessage(object):
def test_reply_poll(self, monkeypatch, message):
def test(*args, **kwargs):
id_ = args[0] == message.chat_id
contact = kwargs['contact'] == 'test_poll'
contact = kwargs['question'] == 'test_poll'
if kwargs.get('reply_to_message_id'):
reply = kwargs['reply_to_message_id'] == message.message_id
else:
@ -710,8 +711,22 @@ class TestMessage(object):
return id_ and contact and reply
monkeypatch.setattr(message.bot, 'send_poll', test)
assert message.reply_poll(contact='test_poll')
assert message.reply_poll(contact='test_poll', quote=True)
assert message.reply_poll(question='test_poll')
assert message.reply_poll(question='test_poll', quote=True)
def test_reply_dice(self, monkeypatch, message):
def test(*args, **kwargs):
id_ = args[0] == message.chat_id
contact = kwargs['disable_notification'] is True
if kwargs.get('reply_to_message_id'):
reply = kwargs['reply_to_message_id'] == message.message_id
else:
reply = True
return id_ and contact and reply
monkeypatch.setattr(message.bot, 'send_dice', test)
assert message.reply_dice(disable_notification=True)
assert message.reply_dice(disable_notification=True, quote=True)
def test_forward(self, monkeypatch, message):
def test(*args, **kwargs):

View file

@ -40,6 +40,19 @@ def sticker(bot, chat_id):
return bot.send_sticker(chat_id, sticker=f, timeout=50).sticker
@pytest.fixture(scope='function')
def animated_sticker_file():
f = open('tests/data/telegram_animated_sticker.tgs', 'rb')
yield f
f.close()
@pytest.fixture(scope='class')
def animated_sticker(bot, chat_id):
with open('tests/data/telegram_animated_sticker.tgs', 'rb') as f:
return bot.send_sticker(chat_id, sticker=f, timeout=50).sticker
class TestSticker(object):
# sticker_file_url = 'https://python-telegram-bot.org/static/testfiles/telegram.webp'
# Serving sticker from gh since our server sends wrong content_type
@ -245,12 +258,27 @@ class TestSticker(object):
@pytest.fixture(scope='function')
def sticker_set(bot):
ss = bot.get_sticker_set('test_by_{0}'.format(bot.username))
ss = bot.get_sticker_set('test_by_{}'.format(bot.username))
if len(ss.stickers) > 100:
raise Exception('stickerset is growing too large.')
return ss
@pytest.fixture(scope='function')
def animated_sticker_set(bot):
ss = bot.get_sticker_set('animated_test_by_{}'.format(bot.username))
if len(ss.stickers) > 100:
raise Exception('stickerset is growing too large.')
return ss
@pytest.fixture(scope='function')
def sticker_set_thumb_file():
f = open('tests/data/sticker_set_thumb.png', 'rb')
yield f
f.close()
class TestStickerSet(object):
title = 'Test stickers'
is_animated = True
@ -258,14 +286,15 @@ class TestStickerSet(object):
stickers = [Sticker('file_id', 'file_un_id', 512, 512, True)]
name = 'NOTAREALNAME'
def test_de_json(self, bot):
name = 'test_by_{0}'.format(bot.username)
def test_de_json(self, bot, sticker):
name = 'test_by_{}'.format(bot.username)
json_dict = {
'name': name,
'title': self.title,
'is_animated': self.is_animated,
'contains_masks': self.contains_masks,
'stickers': [x.to_dict() for x in self.stickers]
'stickers': [x.to_dict() for x in self.stickers],
'thumb': sticker.thumb.to_dict()
}
sticker_set = StickerSet.de_json(json_dict, bot)
@ -274,15 +303,28 @@ class TestStickerSet(object):
assert sticker_set.is_animated == self.is_animated
assert sticker_set.contains_masks == self.contains_masks
assert sticker_set.stickers == self.stickers
assert sticker_set.thumb == sticker.thumb
@flaky(3, 1)
@pytest.mark.timeout(10)
def test_bot_methods_1(self, bot, chat_id):
def test_bot_methods_1_png(self, bot, chat_id, sticker_file):
with open('tests/data/telegram_sticker.png', 'rb') as f:
file = bot.upload_sticker_file(95205500, f)
assert file
assert bot.add_sticker_to_set(chat_id, 'test_by_{0}'.format(bot.username),
file.file_id, '😄')
assert bot.add_sticker_to_set(chat_id, 'test_by_{}'.format(bot.username),
png_sticker=file.file_id, emojis='😄')
# Also test with file input and mask
assert bot.add_sticker_to_set(chat_id, 'test_by_{}'.format(bot.username),
png_sticker=sticker_file, emojis='😄',
mask_position=MaskPosition(MaskPosition.EYES, -1, 1, 2))
@flaky(3, 1)
@pytest.mark.timeout(10)
def test_bot_methods_1_tgs(self, bot, chat_id):
assert bot.add_sticker_to_set(
chat_id, 'animated_test_by_{}'.format(bot.username),
tgs_sticker=open('tests/data/telegram_animated_sticker.tgs', 'rb'),
emojis='😄')
def test_sticker_set_to_dict(self, sticker_set):
sticker_set_dict = sticker_set.to_dict()
@ -296,17 +338,48 @@ class TestStickerSet(object):
@flaky(3, 1)
@pytest.mark.timeout(10)
def test_bot_methods_2(self, bot, sticker_set):
def test_bot_methods_2_png(self, bot, sticker_set):
file_id = sticker_set.stickers[0].file_id
assert bot.set_sticker_position_in_set(file_id, 1)
@flaky(3, 1)
@pytest.mark.timeout(10)
def test_bot_methods_2_tgs(self, bot, animated_sticker_set):
file_id = animated_sticker_set.stickers[0].file_id
assert bot.set_sticker_position_in_set(file_id, 1)
@flaky(10, 1)
@pytest.mark.timeout(10)
def test_bot_methods_3(self, bot, sticker_set):
def test_bot_methods_3_png(self, bot, chat_id, sticker_set_thumb_file):
sleep(1)
assert bot.set_sticker_set_thumb('test_by_{}'.format(bot.username), chat_id,
sticker_set_thumb_file)
@flaky(10, 1)
@pytest.mark.timeout(10)
def test_bot_methods_3_tgs(self, bot, chat_id, animated_sticker_file, animated_sticker_set):
sleep(1)
assert bot.set_sticker_set_thumb('animated_test_by_{}'.format(bot.username), chat_id,
animated_sticker_file)
file_id = animated_sticker_set.stickers[-1].file_id
# also test with file input and mask
assert bot.set_sticker_set_thumb('animated_test_by_{}'.format(bot.username), chat_id,
file_id)
@flaky(10, 1)
@pytest.mark.timeout(10)
def test_bot_methods_4_png(self, bot, sticker_set):
sleep(1)
file_id = sticker_set.stickers[-1].file_id
assert bot.delete_sticker_from_set(file_id)
@flaky(10, 1)
@pytest.mark.timeout(10)
def test_bot_methods_4_tgs(self, bot, animated_sticker_set):
sleep(1)
file_id = animated_sticker_set.stickers[-1].file_id
assert bot.delete_sticker_from_set(file_id)
def test_get_file_instance_method(self, monkeypatch, sticker):
def test(*args, **kwargs):
return args[1] == sticker.file_id