From 7e78b1b6dca3d2c17f142954a19ba2a0b7a50a61 Mon Sep 17 00:00:00 2001 From: Lonami Date: Sun, 11 Sep 2016 13:10:27 +0200 Subject: [PATCH] Added ability to upload and send media, and more fixes Uploading and sending media are different things. Once you have uploaded a media file, you can send it to many users without uploading it again, since you have a handle to the uploaded file. Other fixes include not showing additional data on error messages and not generating correct code for sending bytes --- errors.py | 3 +- main.py | 12 +++++++ tl/telegram_client.py | 74 ++++++++++++++++++++++++++++++++++++++++-- tl_generator.py | 2 +- utils/binary_reader.py | 9 +++++ 5 files changed, 96 insertions(+), 4 deletions(-) diff --git a/errors.py b/errors.py index c493a1da..56e90075 100644 --- a/errors.py +++ b/errors.py @@ -163,10 +163,11 @@ class RPCError(Exception): # Get additional_data, if any if match.groups(): self.additional_data = int(match.group(1)) + super().__init__(self, error_msg.format(self.additional_data)) else: self.additional_data = None + super().__init__(self, error_msg) - super().__init__(self, error_msg) called_super = True break diff --git a/main.py b/main.py index 1346c661..5c7de879 100755 --- a/main.py +++ b/main.py @@ -61,6 +61,7 @@ if __name__ == '__main__': print('You are now sending messages to "{}". Available commands:'.format(display)) print(' !q: Quits the current chat.') print(' !h: prints the latest messages (message History) of the chat.') + print(' !p : sends a Photo located at the given path') # And start a while loop to chat while True: @@ -80,6 +81,17 @@ if __name__ == '__main__': date = datetime.fromtimestamp(msg.date) print('[{}:{}] {}: {}'.format(date.hour, date.minute, name, msg.message)) + # Send photo + elif msg.startswith('!p '): + file_path = msg[len('!p '):] # Slice the message to get the path + + print('Uploading {}...'.format(file_path)) + input_file = client.upload_file(file_path) + + # After we have the handle to the uploaded file, send it to our peer + client.send_photo_file(input_file, input_peer) + print('Media sent!') + # Send chat message (if any) elif msg: client.send_message(input_peer, msg, markdown=True, no_web_page=True) diff --git a/tl/telegram_client.py b/tl/telegram_client.py index 55d76a4a..11a15de6 100644 --- a/tl/telegram_client.py +++ b/tl/telegram_client.py @@ -1,6 +1,9 @@ # This file structure is based on TLSharp # https://github.com/sochix/TLSharp/blob/master/TLSharp.Core/TelegramClient.cs import platform +from datetime import datetime +from hashlib import md5 +from os import path import utils import network.authenticator @@ -12,11 +15,18 @@ from parser.markdown_parser import parse_message_entities # For sending and receiving requests from tl import MTProtoRequest from tl import Session -from tl.types import PeerUser, PeerChat, PeerChannel, InputPeerUser, InputPeerChat, InputPeerChannel, InputPeerEmpty + +# The Requests and types that we'll be using +from tl.types import \ + PeerUser, PeerChat, PeerChannel, \ + InputPeerUser, InputPeerChat, InputPeerChannel, InputPeerEmpty, \ + InputFile, InputMediaUploadedPhoto + from tl.functions import InvokeWithLayerRequest, InitConnectionRequest from tl.functions.help import GetConfigRequest from tl.functions.auth import SendCodeRequest, SignInRequest -from tl.functions.messages import GetDialogsRequest, GetHistoryRequest, SendMessageRequest +from tl.functions.upload import SaveFilePartRequest +from tl.functions.messages import GetDialogsRequest, GetHistoryRequest, SendMessageRequest, SendMediaRequest class TelegramClient: @@ -198,6 +208,66 @@ class TelegramClient: for msg in result.messages # ...from all the messages... for usr in result.users]) # ...from all of the available users + # TODO Handle media downloading/uploading in a different session: + # "It is recommended that large queries (upload.getFile, upload.saveFilePart) + # be handled through a separate session and a separate connection" + def upload_file(self, file_path, part_size_kb=64, file_name=None): + """Uploads the specified media with the given chunk (part) size, in KB. + If no custom file name is specified, the real file name will be used. + This method will fail when trying to upload files larger than 10MB!""" + + part_size = int(part_size_kb * 1024) + if part_size % 1024 != 0: + raise ValueError('The part size must be evenly divisible by 1024') + + # Multiply the datetime timestamp by 10^6 to get the ticks + # This is high likely going to be unique + file_id = int(datetime.now().timestamp() * (10 ** 6)) + part_index = 0 + hash_md5 = md5() + + with open(file_path, 'rb') as file: + while True: + # Read the file by in chunks of size part_size + part = file.read(part_size) + + print('Sending {}'.format(len(part))) + + # If we have read no data (0 bytes), the file is over + # So there is nothing left to upload + if not part: + break + + # Invoke the file upload and increment both the part index and MD5 checksum + result = self.invoke(SaveFilePartRequest(file_id, part_index, part)) + if result: + part_index += 1 + hash_md5.update(part) + else: + raise ValueError('Could not upload file part #{}'.format(part_index)) + + # Set a default file name if None was specified + if not file_name: + file_name = path.basename(file_path) + + # After the file has been uploaded, we can return a handle pointing to it + return InputFile(id=file_id, + parts = part_index, + name=file_name, + md5_checksum=hash_md5.hexdigest()) + + def send_photo_file(self, input_file, input_peer, caption=''): + """Sends a previously uploaded input_file + (which should be a photo) to an input_peer""" + self.send_media_file( + InputMediaUploadedPhoto(input_file, caption), input_peer) + + def send_media_file(self, input_media, input_peer): + """Sends any input_media (contact, document, photo...) to an input_peer""" + self.invoke(SendMediaRequest(peer=input_peer, + media=input_media, + random_id=utils.generate_random_long())) + def invoke(self, request): """Invokes an MTProtoRequest and returns its results""" if not issubclass(type(request), MTProtoRequest): diff --git a/tl_generator.py b/tl_generator.py index 17aa839b..00ce695d 100755 --- a/tl_generator.py +++ b/tl_generator.py @@ -282,7 +282,7 @@ def write_onsend_code(builder, arg, args, name=None): pass # These are actually NOT written! Only used for flags elif 'bytes' == arg.type: - builder.writeln('writer.write({})'.format(name)) + builder.writeln('writer.tgwrite_bytes({})'.format(name)) elif 'date' == arg.type: # Custom format builder.writeln('writer.tgwrite_date({})'.format(name)) diff --git a/utils/binary_reader.py b/utils/binary_reader.py index 0a5c9383..86270941 100755 --- a/utils/binary_reader.py +++ b/utils/binary_reader.py @@ -106,6 +106,15 @@ class BinaryReader: constructor_id = self.read_int(signed=False) clazz = tlobjects.get(constructor_id, None) if clazz is None: + # The class was None, but there's still a + # chance of it being a manually parsed value like bool! + value = constructor_id + if value == 0x997275b5: # boolTrue + return True + elif value == 0xbc799737: # boolFalse + return False + + # If there was still no luck, give up raise TypeNotFoundError(constructor_id) # Now we need to determine the number of parameters of the class, so we can