diff --git a/README.md b/README.md index ef2e753e..7130391d 100755 --- a/README.md +++ b/README.md @@ -34,9 +34,10 @@ def my_function(self, my_arguments): return request.result ``` -5. To determine how the result will look like, simply look at the original `.tl` definition. After the `=`, +To determine how the result will look like, simply look at the original `.tl` definition. After the `=`, you will see the type. Let's see an example: `stickerPack#12b299d4 emoticon:string documents:Vector = StickerPack;` + As it turns out, the result is going to be an `StickerPack`. Without a second doubt, head into `tl/types/` and find it; open the file and see what the result will look like. Alternatively, you can simply `print(str(request.result))`! diff --git a/errors.py b/errors.py index fc0a6a49..5029b9f6 100644 --- a/errors.py +++ b/errors.py @@ -115,6 +115,9 @@ class RPCError(Exception): 'MSG_WAIT_FAILED': 'A waiting call returned an error.', + 'CHAT_ADMIN_REQUIRED': 'Chat admin privileges are required to do that in the specified chat ' + '(for example, to send a message in a channel which is not yours).', + # 401 UNAUTHORIZED 'AUTH_KEY_UNREGISTERED': 'The key is not registered in the system.', diff --git a/main.py b/main.py index d8971381..fb23d788 100755 --- a/main.py +++ b/main.py @@ -8,6 +8,9 @@ if __name__ == '__main__': print('Please run `python3 tl_generator.py` first!') else: + print('Loading interactive example...') + + # First, initialize our TelegramClient and connect settings = load_settings() client = TelegramClient(session_user_id=settings.get('session_name', 'anonymous'), layer=55, @@ -16,11 +19,36 @@ if __name__ == '__main__': client.connect() input('You should now be connected. Press enter when you are ready to continue.') + + # Then, ensure we're authorized and have access if not client.is_user_authorized(): client.send_code_request(str(settings['user_phone'])) code = input('Enter the code you just received: ') client.make_auth(settings['user_phone'], code) - else: - client.get_dialogs() + # After that, load the top dialogs and show a list + # We use zip(*list_of_tuples) to pair all the elements together, + # hence being able to return a new list of each triple pair! + # See http://stackoverflow.com/a/12974504/4759433 for a better explanation + dialogs, displays, inputs = zip(*client.get_dialogs(8)) + + for i, display in enumerate(displays): + i += 1 # 1-based index for normies + print('{}. {}'.format(i, display)) + + # Let the user decide who they want to talk to + i = int(input('Who do you want to send messages to?: ')) - 1 + dialog = dialogs[i] + display = displays[i] + input_peer = inputs[i] + + # And start a while loop! + print('You are now sending messages to "{}". Type "!q" when you want to exit.'.format(display)) + while True: + msg = input('Enter a message: ') + if msg == '!q': + break + client.send_message(input_peer, msg) + + print('Thanks for trying the interactive example! Exiting.') diff --git a/network/mtproto_sender.py b/network/mtproto_sender.py index db11578c..4eba1481 100755 --- a/network/mtproto_sender.py +++ b/network/mtproto_sender.py @@ -13,9 +13,11 @@ from tl.all_tlobjects import tlobjects class MtProtoSender: """MTProto Mobile Protocol sender (https://core.telegram.org/mtproto/description)""" - def __init__(self, transport, session): + def __init__(self, transport, session, swallow_errors=True): self.transport = transport self.session = session + self.swallow_errors = swallow_errors + self.need_confirmation = [] # Message IDs that need confirmation self.on_update_handlers = [] @@ -57,10 +59,17 @@ class MtProtoSender: def receive(self, request): """Receives the specified MTProtoRequest ("fills in it" the received data)""" while not request.confirm_received: - message, remote_msg_id, remote_sequence = self.decode_msg(self.transport.receive().body) + try: + message, remote_msg_id, remote_sequence = self.decode_msg(self.transport.receive().body) - with BinaryReader(message) as reader: - self.process_msg(remote_msg_id, remote_sequence, reader, request) + with BinaryReader(message) as reader: + self.process_msg(remote_msg_id, remote_sequence, reader, request) + + except RPCError as error: + if self.swallow_errors: + print('A RPC error occurred when decoding a message: {}'.format(error)) + else: + raise error # endregion diff --git a/tl/telegram_client.py b/tl/telegram_client.py index 77638c76..c8f923dc 100644 --- a/tl/telegram_client.py +++ b/tl/telegram_client.py @@ -1,6 +1,7 @@ # This file is based on TLSharp # https://github.com/sochix/TLSharp/blob/master/TLSharp.Core/TelegramClient.cs import platform +import datetime import utils import network.authenticator @@ -8,7 +9,7 @@ from network import MtProtoSender, TcpTransport from errors import * from tl import Session -from tl.types import InputPeerUser, InputPeerEmpty +from tl.types import PeerUser, PeerChat, PeerChannel, InputPeerUser, InputPeerChat, InputPeerChannel, InputPeerEmpty from tl.functions import InvokeWithLayerRequest, InitConnectionRequest from tl.functions.help import GetConfigRequest from tl.functions.auth import CheckPhoneRequest, SendCodeRequest, SignInRequest @@ -16,6 +17,9 @@ from tl.functions.messages import GetDialogsRequest, SendMessageRequest class TelegramClient: + + # region Initialization + def __init__(self, session_user_id, layer, api_id=None, api_hash=None): if api_id is None or api_hash is None: raise PermissionError('Your API ID or Hash are invalid. Please read "Requirements" on README.md') @@ -31,9 +35,17 @@ class TelegramClient: # These will be set later self.dc_options = None self.sender = None - self.phone_code_hash = None + self.phone_code_hashes = {} + + # endregion + + # region Connecting def connect(self, reconnect=False): + """Connects to the Telegram servers, executing authentication if required. + Note that authenticating to the Telegram servers is not the same as authenticating + the app, which requires to send a code first.""" + if not self.session.auth_key or reconnect: self.session.auth_key, self.session.time_offset = network.authenticator.do_authentication(self.transport) self.session.save() @@ -48,23 +60,21 @@ class TelegramClient: query=InitConnectionRequest(api_id=self.api_id, device_model=platform.node(), system_version=platform.system(), - app_version='0.1', + app_version='0.2', lang_code='en', query=GetConfigRequest())) self.sender.send(request) self.sender.receive(request) - # Result is a Config TLObject self.dc_options = request.result.dc_options - return True def reconnect_to_dc(self, dc_id): + """Reconnects to the specified DC ID. This is automatically called after an InvalidDCError is raised""" if self.dc_options is None or not self.dc_options: raise ConnectionError("Can't reconnect. Stabilise an initial connection first.") - # dc is a DcOption TLObject dc = next(dc for dc in self.dc_options if dc.id == dc_id) self.transport.close() @@ -75,20 +85,17 @@ class TelegramClient: self.connect(reconnect=True) + # endregion + + # region Telegram requests functions + def is_user_authorized(self): + """Has the user been authorized yet (code request sent and confirmed)? + Note that this will NOT yield the correct result if the session was revoked by another client!""" return self.session.user is not None - def is_phone_registered(self, phone_number): - assert self.sender is not None, 'Not connected!' - - request = CheckPhoneRequest(phone_number) - self.sender.send(request) - self.sender.receive(request) - - # Result is an Auth.CheckedPhone - return request.result.phone_registered - def send_code_request(self, phone_number): + """Sends a code request to the specified phone number""" request = SendCodeRequest(phone_number, self.api_id, self.api_hash) completed = False while not completed: @@ -97,16 +104,18 @@ class TelegramClient: self.sender.receive(request) completed = True if request.result: - self.phone_code_hash = request.result.phone_code_hash + self.phone_code_hashes[phone_number] = request.result.phone_code_hash except InvalidDCError as error: self.reconnect_to_dc(error.new_dc) def make_auth(self, phone_number, code): - if not self.phone_code_hash: - raise ValueError('Please make sure you have called send_code_request first!') + """Completes the authorization of a phone number by providing the received code""" + if phone_number not in self.phone_code_hashes: + raise ValueError('Please make sure you have called send_code_request first.') - request = SignInRequest(phone_number, self.phone_code_hash, code) + # TODO Handle invalid code + request = SignInRequest(phone_number, self.phone_code_hashes[phone_number], code) self.sender.send(request) self.sender.receive(request) @@ -116,25 +125,92 @@ class TelegramClient: return self.session.user - def get_dialogs(self): - request = GetDialogsRequest(offset_date=0, - offset_id=0, - offset_peer=InputPeerEmpty(), - limit=20) + def get_dialogs(self, count=10, offset_date=None, offset_id=0, offset_peer=InputPeerEmpty()): + """Returns 'count' dialogs in a (dialog, display, input_peer) list format""" + + # Telegram wants the offset_date in an unix-timestamp format, not Python's datetime + # However that's not very comfortable, so calculate the correct value here + if offset_date is None: + offset_date = 0 + else: + offset_date = int(offset_date.timestamp()) + + request = GetDialogsRequest(offset_date=offset_date, + offset_id=offset_id, + offset_peer=offset_peer, + limit=count) self.sender.send(request) self.sender.receive(request) - print(request.result) + result = request.result + return [(dialog, + TelegramClient.find_display_name(dialog.peer, result.users, result.chats), + TelegramClient.find_input_peer_name(dialog.peer, result.users, result.chats)) + for dialog in result.dialogs] - def send_message(self, user, message): - peer = InputPeerUser(user.id, user.access_hash) - request = SendMessageRequest(peer, message, utils.generate_random_long()) + def send_message(self, input_peer, message): + """Sends a message to the given input peer""" + request = SendMessageRequest(input_peer, message, utils.generate_random_long()) self.sender.send(request) self.sender.receive(request) + # endregion + + # region Utilities + + @staticmethod + def find_display_name(peer, users, chats): + """Searches the display name for peer in both users and chats. + Returns None if it was not found""" + try: + if type(peer) is PeerUser: + user = next(u for u in users if u.id == peer.user_id) + if user.last_name is not None: + return '{} {}'.format(user.first_name, user.last_name) + return user.first_name + + elif type(peer) is PeerChat: + return next(c for c in chats if c.id == peer.chat_id).title + + elif type(peer) is PeerChannel: + return next(c for c in chats if c.id == peer.channel_id).title + + except StopIteration: + pass + + return None + + @staticmethod + def find_input_peer_name(peer, users, chats): + """Searches the given peer in both users and chats and returns an InputPeer for it. + Returns None if it was not found""" + try: + if type(peer) is PeerUser: + user = next(u for u in users if u.id == peer.user_id) + return InputPeerUser(user.id, user.access_hash) + + elif type(peer) is PeerChat: + chat = next(c for c in chats if c.id == peer.chat_id) + return InputPeerChat(chat.id) + + elif type(peer) is PeerChannel: + channel = next(c for c in chats if c.id == peer.channel_id) + return InputPeerChannel(channel.id, channel.access_hash) + + except StopIteration: + pass + + return None + + # endregion + + # region Updates handling + def on_update(self, tlobject): """This method is fired when there are updates from Telegram. Add your own implementation below, or simply override it!""" print('We have an update: {}'.format(str(tlobject))) + + # endregion diff --git a/tl/tlobject.py b/tl/tlobject.py index 6c1566f6..adf9e7f1 100755 --- a/tl/tlobject.py +++ b/tl/tlobject.py @@ -94,7 +94,11 @@ class TLObject: if not arg.flag_indicator and not arg.generic_definition] args = ', '.join(['{}={{}}'.format(arg.name) for arg in valid_args]) - args_format = ', '.join(['str(self.{})'.format(arg.name) for arg in valid_args]) + + # Since Python's default representation for lists is using repr(), we need to str() manually on every item + args_format = ', '.join(['str(self.{})'.format(arg.name) if not arg.is_vector else + 'None if not self.{0} else [str(_) for _ in self.{0}]'.format(arg.name) + for arg in valid_args]) return ("'({} (ID: {}) = ({}))'.format({})" .format(fullname, hex(self.id), args, args_format))