diff --git a/telegram/__init__.py b/telegram/__init__.py index 3ef6cda08..adba83334 100644 --- a/telegram/__init__.py +++ b/telegram/__init__.py @@ -94,6 +94,7 @@ from .precheckoutquery import PreCheckoutQuery from .shippingquery import ShippingQuery from .webhookinfo import WebhookInfo from .gamehighscore import GameHighScore +from .videonote import VideoNote from .update import Update from .bot import Bot from .constants import (MAX_MESSAGE_LENGTH, MAX_CAPTION_LENGTH, SUPPORTED_WEBHOOK_PORTS, @@ -123,6 +124,6 @@ __all__ = [ 'Video', 'Voice', 'MAX_MESSAGE_LENGTH', 'MAX_CAPTION_LENGTH', 'SUPPORTED_WEBHOOK_PORTS', 'MAX_FILESIZE_DOWNLOAD', 'MAX_FILESIZE_UPLOAD', 'MAX_MESSAGES_PER_SECOND_PER_CHAT', 'MAX_MESSAGES_PER_SECOND', 'MAX_MESSAGES_PER_MINUTE_PER_GROUP', 'WebhookInfo', 'Animation', - 'Game', 'GameHighScore', 'LabeledPrice', 'SuccessfulPayment', 'ShippingOption', + 'Game', 'GameHighScore', 'VideoNote', 'LabeledPrice', 'SuccessfulPayment', 'ShippingOption', 'ShippingAddress', 'PreCheckoutQuery', 'OrderInfo', 'Invoice', 'ShippingQuery' ] diff --git a/telegram/bot.py b/telegram/bot.py index ee009bf10..d0713613d 100644 --- a/telegram/bot.py +++ b/telegram/bot.py @@ -671,6 +671,66 @@ class Bot(TelegramObject): timeout=timeout, **kwargs) + @log + def send_video_note(self, + chat_id, + video_note, + duration=None, + length=None, + disable_notification=False, + reply_to_message_id=None, + reply_markup=None, + timeout=20., + **kwargs): + """As of v.4.0, Telegram clients support rounded square mp4 videos of up to 1 minute + long. Use this method to send video messages + + Args: + chat_id (int|str): Unique identifier for the message recipient - Chat id. + voice: Video note to send. Pass a file_id as String to send a video note that exists + on the Telegram servers (recommended) or upload a new video. Sending video notes + by a URL is currently unsupported + duration (Optional[int]): Duration of sent audio in seconds. + length (Optional[int]): Video width and height + disable_notification (Optional[bool]): Sends the message silently. iOS users will not + receive a notification, Android users will receive a notification with no sound. + reply_to_message_id (Optional[int]): If the message is a reply, ID of the original + message. + reply_markup (Optional[:class:`telegram.ReplyMarkup`]): 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 (Optional[int|float]): Send file timeout (default: 20 seconds). + **kwargs (dict): Arbitrary keyword arguments. + + Returns: + :class:`telegram.Message`: On success, instance representing the message posted. + + Raises: + :class:`telegram.TelegramError` + + """ + url = '{0}/sendVideoNote'.format(self.base_url) + + data = {'chat_id': chat_id, 'video_note': video_note} + + if duration: + data['duration'] = duration + if length: + data['length'] = length + + return self._message_wrapper( + url, + data, + chat_id=chat_id, + video_note=video_note, + duration=duration, + length=length, + disable_notification=disable_notification, + reply_to_message_id=reply_to_message_id, + reply_markup=reply_markup, + timeout=timeout, + **kwargs) + @log @message def send_location(self, @@ -1963,6 +2023,7 @@ class Bot(TelegramObject): sendSticker = send_sticker sendVideo = send_video sendVoice = send_voice + sendVideoNote = send_video_note sendLocation = send_location sendVenue = send_venue sendContact = send_contact diff --git a/telegram/chataction.py b/telegram/chataction.py index 99b39139f..df6e99a9e 100644 --- a/telegram/chataction.py +++ b/telegram/chataction.py @@ -31,3 +31,5 @@ class ChatAction(object): UPLOAD_AUDIO = 'upload_audio' UPLOAD_DOCUMENT = 'upload_document' FIND_LOCATION = 'find_location' + RECORD_VIDEO_NOTE = 'record_video_note' + UPLOAD_VIDEO_NOTE = 'upload_video_note' diff --git a/telegram/inputfile.py b/telegram/inputfile.py index e73115f79..de4aac011 100644 --- a/telegram/inputfile.py +++ b/telegram/inputfile.py @@ -35,7 +35,8 @@ from telegram import TelegramError DEFAULT_MIME_TYPE = 'application/octet-stream' USER_AGENT = 'Python Telegram Bot (https://github.com/python-telegram-bot/python-telegram-bot)' -FILE_TYPES = ('audio', 'document', 'photo', 'sticker', 'video', 'voice', 'certificate') +FILE_TYPES = ('audio', 'document', 'photo', 'sticker', 'video', 'voice', 'certificate', + 'video_note') class InputFile(object): @@ -66,6 +67,9 @@ class InputFile(object): elif 'certificate' in data: self.input_name = 'certificate' self.input_file = data.pop('certificate') + elif 'video_note' in data: + self.input_name = 'video_note' + self.input_file = data.pop('video_note') else: raise TelegramError('Unknown inputfile type') @@ -117,7 +121,7 @@ class InputFile(object): form_boundary, 'Content-Disposition: form-data; name="%s"' % name, '', str(value) ]) - # Add input_file to upload +# Add input_file to upload form.extend([ form_boundary, 'Content-Disposition: form-data; name="%s"; filename="%s"' % (self.input_name, diff --git a/telegram/message.py b/telegram/message.py index 9f9050076..8c6694de5 100644 --- a/telegram/message.py +++ b/telegram/message.py @@ -23,7 +23,8 @@ from datetime import datetime from time import mktime from telegram import (Audio, Contact, Document, Chat, Location, PhotoSize, Sticker, TelegramObject, - User, Video, Voice, Venue, MessageEntity, Game, Invoice, SuccessfulPayment) + User, Video, Voice, Venue, MessageEntity, Game, Invoice, SuccessfulPayment, + VideoNote) from telegram.utils.helpers import escape_html, escape_markdown @@ -62,6 +63,8 @@ class Message(TelegramObject): sticker (:class:`telegram.Sticker`): Message is a sticker, information about the sticker video (:class:`telegram.Video`): Message is a video, information about the video voice (:class:`telegram.Voice`): Message is a voice message, information about the file + video_note (:class:`telegram.VideoNote`): Message is a video note, information about the + video message caption (str): Caption for the document, photo or video, 0-200 characters contact (:class:`telegram.Contact`): Message is a shared contact, information about the contact @@ -123,6 +126,7 @@ class Message(TelegramObject): sticker=None, video=None, voice=None, + video_note=None, caption=None, contact=None, location=None, @@ -163,6 +167,7 @@ class Message(TelegramObject): self.sticker = sticker self.video = video self.voice = voice + self.video_note = video_note self.caption = caption self.contact = contact self.location = location @@ -222,6 +227,7 @@ class Message(TelegramObject): data['sticker'] = Sticker.de_json(data.get('sticker'), bot) data['video'] = Video.de_json(data.get('video'), bot) data['voice'] = Voice.de_json(data.get('voice'), bot) + data['video_note'] = VideoNote.de_json(data.get('video_note'), bot) data['contact'] = Contact.de_json(data.get('contact'), bot) data['location'] = Location.de_json(data.get('location'), bot) data['venue'] = Venue.de_json(data.get('venue'), bot) @@ -412,6 +418,23 @@ class Message(TelegramObject): self._quote(kwargs) return self.bot.sendVideo(self.chat_id, *args, **kwargs) + def reply_video_note(self, *args, **kwargs): + """ + Shortcut for ``bot.send_video_note(update.message.chat_id, *args, **kwargs)`` + + Keyword Args: + quote (Optional[bool]): If set to ``True``, the video 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_video_note(self.chat_id, *args, **kwargs) + def reply_voice(self, *args, **kwargs): """ Shortcut for ``bot.sendVoice(update.message.chat_id, *args, **kwargs)`` diff --git a/telegram/videonote.py b/telegram/videonote.py new file mode 100644 index 000000000..ba3616d43 --- /dev/null +++ b/telegram/videonote.py @@ -0,0 +1,63 @@ +#!/usr/bin/env python +# +# A library that provides a Python interface to the Telegram Bot API +# Copyright (C) 2015-2017 +# Leandro Toledo de Souza +# +# 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 VideoNote.""" + +from telegram import PhotoSize, TelegramObject + + +class VideoNote(TelegramObject): + """This object represents a Telegram VideoNote. + + Attributes: + file_id (str): Unique identifier for this file + length (int): Video width and height as defined by sender + duration (int): Duration of the video in seconds as defined by sender + thumb (Optional[:class:`telegram.PhotoSize`]): Video thumbnail + file_size (Optional[int]): File size + """ + + def __init__(self, file_id, length, duration, thumb=None, file_size=None, **kwargs): + # Required + self.file_id = str(file_id) + self.length = int(length) + self.duration = int(duration) + # Optionals + self.thumb = thumb + self.file_size = file_size + + self._id_attrs = (self.file_id,) + + @staticmethod + def de_json(data, bot): + """ + Args: + data (dict): + bot (telegram.Bot): + + Returns: + telegram.VideoNote: + """ + if not data: + return None + + data = super(VideoNote, VideoNote).de_json(data, bot) + + data['thumb'] = PhotoSize.de_json(data.get('thumb'), bot) + + return VideoNote(**data) diff --git a/tests/test_videonote.py b/tests/test_videonote.py new file mode 100644 index 000000000..5689157bb --- /dev/null +++ b/tests/test_videonote.py @@ -0,0 +1,212 @@ +#!/usr/bin/env python +# +# A library that provides a Python interface to the Telegram Bot API +# Copyright (C) 2015-2017 +# Leandro Toledo de Souza +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General 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 General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see [http://www.gnu.org/licenses/]. +"""This module contains an object that represents Tests for Telegram VideoNote""" + +import sys +import unittest +import os + +from flaky import flaky + +sys.path.append('.') + +import telegram +from tests.base import BaseTest, timeout + + +class VideoNoteTest(BaseTest, unittest.TestCase): + """This object represents Tests for Telegram VideoNote.""" + + def setUp(self): + self.videonote_file = open('tests/data/telegram.mp4', 'rb') + self.videonote_file_id = 'DQADAQADBwAD5VIIRYemhHpbPmIQAg' + self.duration = 5 + self.length = 1 # No bloody clue what this does, see note in first test + self.thumb = telegram.PhotoSize.de_json({ + 'file_id': 'AAQBABOMsecvAAQqqoY1Pee_MqcyAAIC', + 'width': 51, + 'file_size': 645, + 'height': 90 + }, self._bot) + self.file_size = 326534 + + self.json_dict = { + 'file_id': self.videonote_file_id, + 'duration': self.duration, + 'length': self.length, + 'thumb': self.thumb.to_dict(), + 'file_size': self.file_size + } + + @flaky(3, 1) + @timeout(10) + def test_error_send_videonote_required_args_only(self): + # This is where it gets weird.... + # According to telegram length is Video width and height.. but how that works with one + # parameter I couldn't tell you + # It would also seem that it is in fact a required parameter, so the original test below + # fails. Therefore I decided I check for the error instead - that way we'll also know + # when telegram fixes their shit + with self.assertRaisesRegexp(telegram.error.BadRequest, r'Wrong video note length'): + message = self._bot.sendVideoNote(self._chat_id, self.videonote_file, timeout=10) + + # videonote = message.videonote + # + # self.assertTrue(isinstance(videonote.file_id, str)) + # self.assertNotEqual(videonote.file_id, None) + # self.assertEqual(videonote.duration, self.duration) + # self.assertEqual(videonote.length, self.length) + # self.assertEqual(videonote.thumb, self.thumb) + # self.assertEqual(videonote.file_size, self.file_size) + + @flaky(3, 1) + @timeout(10) + def test_send_videonote_actual_required_args_only(self): + # See above test... if you pass any number that's > 0 and < some high number, it seems + # to work + message = self._bot.sendVideoNote( + self._chat_id, self.videonote_file, length=self.length, timeout=10) + + videonote = message.video_note + + self.assertTrue(isinstance(videonote.file_id, str)) + self.assertNotEqual(videonote.file_id, None) + self.assertEqual(videonote.duration, self.duration) + # self.assertEqual(videonote.length, self.length) + self.assertEqual(videonote.thumb, self.thumb) + self.assertEqual(videonote.file_size, self.file_size) + + @flaky(3, 1) + @timeout(10) + def test_send_videonote_all_args(self): + message = self._bot.sendVideoNote( + self._chat_id, + self.videonote_file, + timeout=10, + duration=self.duration, + length=self.length) + + videonote = message.video_note + + self.assertTrue(isinstance(videonote.file_id, str)) + self.assertNotEqual(videonote.file_id, None) + # self.assertEqual(videonote.length, self.length) + self.assertEqual(videonote.duration, self.duration) + self.assertEqual(videonote.thumb, self.thumb) + self.assertEqual(videonote.file_size, self.file_size) + + @flaky(3, 1) + @timeout(10) + def test_send_videonote_resend(self): + message = self._bot.sendVideoNote( + chat_id=self._chat_id, + video_note=self.videonote_file_id, + timeout=10, + duration=self.duration, + length=self.length) + + videonote = message.video_note + + self.assertEqual(videonote.file_id, self.videonote_file_id) + # self.assertEqual(videonote.length, self.length) + self.assertEqual(videonote.duration, self.duration) + self.assertEqual(videonote.thumb, self.thumb) + self.assertEqual(videonote.file_size, self.file_size) + + def test_videonote_de_json(self): + videonote = telegram.VideoNote.de_json(self.json_dict, self._bot) + + self.assertEqual(videonote.file_id, self.videonote_file_id) + # self.assertEqual(videonote.duration, self.duration) + self.assertEqual(videonote.thumb, self.thumb) + self.assertEqual(videonote.length, self.length) + self.assertEqual(videonote.file_size, self.file_size) + + def test_videonote_to_json(self): + videonote = telegram.VideoNote.de_json(self.json_dict, self._bot) + + self.assertTrue(self.is_json(videonote.to_json())) + + def test_videonote_to_dict(self): + videonote = telegram.VideoNote.de_json(self.json_dict, self._bot) + + self.assertTrue(self.is_dict(videonote.to_dict())) + self.assertEqual(videonote['file_id'], self.videonote_file_id) + self.assertEqual(videonote['duration'], self.duration) + self.assertEqual(videonote['length'], self.length) + self.assertEqual(videonote['file_size'], self.file_size) + + @flaky(3, 1) + @timeout(10) + def test_error_send_videonote_empty_file(self): + json_dict = self.json_dict + + del (json_dict['file_id']) + json_dict['video_note'] = open(os.devnull, 'rb') + + self.assertRaises(telegram.TelegramError, + lambda: self._bot.sendVideoNote(chat_id=self._chat_id, + timeout=10, + **json_dict)) + + @flaky(3, 1) + @timeout(10) + def test_error_send_videonote_empty_file_id(self): + json_dict = self.json_dict + + del (json_dict['file_id']) + json_dict['video_note'] = '' + + self.assertRaises(telegram.TelegramError, + lambda: self._bot.sendVideoNote(chat_id=self._chat_id, + timeout=10, + **json_dict)) + + @flaky(3, 1) + @timeout(10) + def test_reply_videonote(self): + """Test for Message.reply_videonote""" + message = self._bot.sendMessage(self._chat_id, '.') + # Length is needed... see first test + message = message.reply_video_note(self.videonote_file, length=self.length) + + self.assertNotEqual(message.video_note.file_id, None) + + def test_equality(self): + a = telegram.VideoNote(self.videonote_file_id, self.length, self.duration) + b = telegram.VideoNote(self.videonote_file_id, self.length, self.duration) + c = telegram.VideoNote(self.videonote_file_id, 0, 0, 0) + d = telegram.VideoNote("", self.length, self.duration) + e = telegram.Voice(self.videonote_file_id, self.duration) + + self.assertEqual(a, b) + self.assertEqual(hash(a), hash(b)) + self.assertIsNot(a, b) + + self.assertEqual(a, c) + self.assertEqual(hash(a), hash(c)) + + self.assertNotEqual(a, d) + self.assertNotEqual(hash(a), hash(d)) + + self.assertNotEqual(a, e) + self.assertNotEqual(hash(a), hash(e)) + +if __name__ == '__main__': + unittest.main()