diff --git a/readthedocs/telethon.client.rst b/readthedocs/telethon.client.rst index 3803f0d2..b5378020 100644 --- a/readthedocs/telethon.client.rst +++ b/readthedocs/telethon.client.rst @@ -20,7 +20,7 @@ its own methods, which you all can use. # Now you can use all client methods listed below, like for example... await client.send_message('me', 'Hello to myself!') - asyncio.get_event_loop().run_until_complete() + asyncio.get_event_loop().run_until_complete(main()) You **don't** need to import these `AuthMethods`, `MessageMethods`, etc. diff --git a/telethon_examples/interactive_telegram_client.py b/telethon_examples/interactive_telegram_client.py index 12e1b298..382ca0cd 100644 --- a/telethon_examples/interactive_telegram_client.py +++ b/telethon_examples/interactive_telegram_client.py @@ -1,4 +1,6 @@ import os +import sys +import asyncio from getpass import getpass from telethon.utils import get_display_name @@ -6,9 +8,10 @@ from telethon.utils import get_display_name from telethon import TelegramClient, events from telethon.network import ConnectionTcpAbridged from telethon.errors import SessionPasswordNeededError -from telethon.tl.types import ( - PeerChat, UpdateShortChatMessage, UpdateShortMessage -) + + +# Create a global variable to hold the loop we will be using +loop = asyncio.get_event_loop() def sprint(string, *args, **kwargs): @@ -41,6 +44,16 @@ def bytes_to_string(byte_count): ) +async def async_input(prompt): + """ + Python's ``input()`` is blocking, which means the event loop we set + above can't be running while we're blocking there. This method will + let the loop run while we wait for input. + """ + print(prompt, end='', flush=True) + return (await loop.run_in_executor(None, sys.stdin.readline)).rstrip() + + class InteractiveTelegramClient(TelegramClient): """Full featured Telegram client, meant to be used on an interactive session to see what Telethon is capable off - @@ -78,41 +91,37 @@ class InteractiveTelegramClient(TelegramClient): connection=ConnectionTcpAbridged, # If you're using a proxy, set it here. - proxy=proxy, - - # If you want to receive updates, you need to start one or more - # "update workers" which are background threads that will allow - # you to run things when your update handlers (callbacks) are - # called with an Update object. - update_workers=1 + proxy=proxy ) # Store {message.id: message} map here so that we can download # media known the message ID, for every message having media. self.found_media = {} - # Calling .connect() may return False, so you need to assert it's - # True before continuing. Otherwise you may want to retry as done here. + # Calling .connect() may raise a connection error False, so you need + # to except those before continuing. Otherwise you may want to retry + # as done here. print('Connecting to Telegram servers...') - if not self.connect(): + try: + loop.run_until_complete(self.connect()) + except ConnectionError: print('Initial connection failed. Retrying...') - if not self.connect(): - print('Could not connect to Telegram servers.') - return + loop.run_until_complete(self.connect()) # If the user hasn't called .sign_in() or .sign_up() yet, they won't # be authorized. The first thing you must do is authorize. Calling # .sign_in() should only be done once as the information is saved on # the *.session file so you don't need to enter the code every time. - if not self.is_user_authorized(): + if not loop.run_until_complete(self.is_user_authorized()): print('First run. Sending code request...') - self.sign_in(user_phone) + loop.run_until_complete(self.sign_in(user_phone)) self_user = None while self_user is None: code = input('Enter the code you just received: ') try: - self_user = self.sign_in(code=code) + self_user =\ + loop.run_until_complete(self.sign_in(code=code)) # Two-step verification may be enabled, and .sign_in will # raise this error. If that's the case ask for the password. @@ -122,9 +131,10 @@ class InteractiveTelegramClient(TelegramClient): pw = getpass('Two step verification is enabled. ' 'Please enter your password: ') - self_user = self.sign_in(password=pw) + self_user =\ + loop.run_until_complete(self.sign_in(password=pw)) - def run(self): + async def run(self): """Main loop of the TelegramClient, will wait for user action""" # Once everything is ready, we can add an event handler. @@ -142,7 +152,7 @@ class InteractiveTelegramClient(TelegramClient): # Entities represent the user, chat or channel # corresponding to the dialog on the same index. - dialogs = self.get_dialogs(limit=dialog_count) + dialogs = await self.get_dialogs(limit=dialog_count) i = None while i is None: @@ -159,7 +169,7 @@ class InteractiveTelegramClient(TelegramClient): print(' !q: Quits the dialogs window and exits.') print(' !l: Logs out, terminating this session.') print() - i = input('Enter dialog ID or a command: ') + i = await async_input('Enter dialog ID or a command: ') if i == '!q': return if i == '!l': @@ -169,7 +179,7 @@ class InteractiveTelegramClient(TelegramClient): # # This is not the same as simply calling .disconnect(), # which simply shuts down everything gracefully. - self.log_out() + await self.log_out() return try: @@ -199,7 +209,7 @@ class InteractiveTelegramClient(TelegramClient): # And start a while loop to chat while True: - msg = input('Enter a message: ') + msg = await async_input('Enter a message: ') # Quit if msg == '!q': break @@ -209,16 +219,16 @@ class InteractiveTelegramClient(TelegramClient): # History elif msg == '!h': # First retrieve the messages and some information - messages = self.get_messages(entity, limit=10) + messages = await self.get_messages(entity, limit=10) # Iterate over all (in reverse order so the latest appear # the last in the console) and print them with format: # "[hh:mm] Sender: Message" for msg in reversed(messages): - # Note that the .sender attribute is only there for - # convenience, the API returns it differently. But - # this shouldn't concern us. See the documentation - # for .iter_messages() for more information. + # Note how we access .sender here. Since we made an + # API call using the self client, it will always have + # information about the sender. This is different to + # events, where Telegram may not always send the user. name = get_display_name(msg.sender) # Format the message content @@ -242,29 +252,33 @@ class InteractiveTelegramClient(TelegramClient): # Send photo elif msg.startswith('!up '): # Slice the message to get the path - self.send_photo(path=msg[len('!up '):], entity=entity) + path = msg[len('!up '):] + await self.send_photo(path=path, entity=entity) # Send file (document) elif msg.startswith('!uf '): # Slice the message to get the path - self.send_document(path=msg[len('!uf '):], entity=entity) + path = msg[len('!uf '):] + await self.send_document(path=path, entity=entity) # Delete messages elif msg.startswith('!d '): # Slice the message to get message ID - deleted_msg = self.delete_messages(entity, msg[len('!d '):]) + msg = msg[len('!d '):] + deleted_msg = await self.delete_messages(entity, msg) print('Deleted {}'.format(deleted_msg)) # Download media elif msg.startswith('!dm '): # Slice the message to get message ID - self.download_media_by_id(msg[len('!dm '):]) + await self.download_media_by_id(msg[len('!dm '):]) # Download profile photo elif msg == '!dp': print('Downloading profile picture to usermedia/...') os.makedirs('usermedia', exist_ok=True) - output = self.download_profile_photo(entity, 'usermedia') + output = await self.download_profile_photo(entity, + 'usermedia') if output: print('Profile picture downloaded to', output) else: @@ -278,26 +292,26 @@ class InteractiveTelegramClient(TelegramClient): # Send chat message (if any) elif msg: - self.send_message(entity, msg, link_preview=False) + await self.send_message(entity, msg, link_preview=False) - def send_photo(self, path, entity): + async def send_photo(self, path, entity): """Sends the file located at path to the desired entity as a photo""" - self.send_file( + await self.send_file( entity, path, progress_callback=self.upload_progress_callback ) print('Photo sent!') - def send_document(self, path, entity): + async def send_document(self, path, entity): """Sends the file located at path to the desired entity as a document""" - self.send_file( + await self.send_file( entity, path, force_document=True, progress_callback=self.upload_progress_callback ) print('Document sent!') - def download_media_by_id(self, media_id): + async def download_media_by_id(self, media_id): """Given a message ID, finds the media this message contained and downloads it. """ @@ -310,7 +324,7 @@ class InteractiveTelegramClient(TelegramClient): print('Downloading media to usermedia/...') os.makedirs('usermedia', exist_ok=True) - output = self.download_media( + output = await self.download_media( msg.media, file='usermedia/', progress_callback=self.download_progress_callback @@ -336,29 +350,33 @@ class InteractiveTelegramClient(TelegramClient): bytes_to_string(total_bytes), downloaded_bytes / total_bytes) ) - def message_handler(self, event): + async def message_handler(self, event): """Callback method for received events.NewMessage""" - # Note that accessing ``.sender`` and ``.chat`` may be slow since - # these are not cached and must be queried always! However it lets - # us access the chat title and user name. + # Note that message_handler is called when a Telegram update occurs + # and an event is created. Telegram may not always send information + # about the ``.sender`` or the ``.chat``, so if you *really* want it + # you should use ``get_chat()`` and ``get_sender()`` while working + # with events. Since they are methods, you know they may make an API + # call, which can be expensive. + chat = await event.get_chat() if event.is_group: if event.out: sprint('>> sent "{}" to chat {}'.format( - event.text, get_display_name(event.chat) + event.text, get_display_name(chat) )) else: sprint('<< {} @ {} sent "{}"'.format( - get_display_name(event.sender), - get_display_name(event.chat), + get_display_name(await event.get_sender()), + get_display_name(chat), event.text )) else: if event.out: sprint('>> "{}" to user {}'.format( - event.text, get_display_name(event.chat) + event.text, get_display_name(chat) )) else: sprint('<< {} sent "{}"'.format( - get_display_name(event.chat), event.text + get_display_name(chat), event.text )) diff --git a/telethon_examples/print_updates.py b/telethon_examples/print_updates.py index fff8f7e4..c62c518b 100755 --- a/telethon_examples/print_updates.py +++ b/telethon_examples/print_updates.py @@ -5,6 +5,7 @@ # your environment variables. This is a good way to use these private # values. See https://superuser.com/q/284342. +import asyncio from os import environ # environ is used to get API information from environment variables @@ -13,28 +14,27 @@ from os import environ from telethon import TelegramClient -def main(): +async def main(): session_name = environ.get('TG_SESSION', 'session') client = TelegramClient(session_name, int(environ['TG_API_ID']), environ['TG_API_HASH'], - proxy=None, - update_workers=4, - spawn_read_thread=False) + proxy=None) if 'TG_PHONE' in environ: - client.start(phone=environ['TG_PHONE']) + await client.start(phone=environ['TG_PHONE']) else: - client.start() + await client.start() client.add_event_handler(update_handler) print('(Press Ctrl+C to stop this)') - client.idle() + await client.disconnected -def update_handler(update): +async def update_handler(update): print(update) if __name__ == '__main__': - main() + loop = asyncio.get_event_loop() + loop.run_until_complete(main()) diff --git a/telethon_examples/replier.py b/telethon_examples/replier.py index 312ef2f1..2293542f 100755 --- a/telethon_examples/replier.py +++ b/telethon_examples/replier.py @@ -10,11 +10,12 @@ This script assumes that you have certain files on the working directory, such as "xfiles.m4a" or "anytime.png" for some of the automated replies. """ import re +import asyncio from collections import defaultdict from datetime import datetime, timedelta from os import environ -from telethon import TelegramClient, events, utils +from telethon import TelegramClient, events """Uncomment this for debugging import logging @@ -30,57 +31,54 @@ REACTS = {'emacs': 'Needs more vim', recent_reacts = defaultdict(list) +# TG_API_ID and TG_API_HASH *must* exist or this won't run! +session_name = environ.get('TG_SESSION', 'session') +client = TelegramClient( + session_name, int(environ['TG_API_ID']), environ['TG_API_HASH'], + proxy=None +) + + +@client.on(events.NewMessage) +async def my_handler(event: events.NewMessage.Event): + global recent_reacts + + # Through event.raw_text we access the text of messages without format + words = re.split('\W+', event.raw_text) + + # Try to match some reaction + for trigger, response in REACTS.items(): + if len(recent_reacts[event.chat_id]) > 3: + # Silently ignore triggers if we've recently sent 3 reactions + break + + if trigger in words: + # Remove recent replies older than 10 minutes + recent_reacts[event.chat_id] = [ + a for a in recent_reacts[event.chat_id] if + datetime.now() - a < timedelta(minutes=10) + ] + # Send a reaction as a reply (otherwise, event.respond()) + await event.reply(response) + # Add this reaction to the list of recent actions + recent_reacts[event.chat_id].append(datetime.now()) + + # Automatically send relevant media when we say certain things + # When invoking requests, get_input_entity needs to be called manually + if event.out: + chat = await event.get_input_chat() + if event.raw_text.lower() == 'x files theme': + await client.send_file(chat, 'xfiles.m4a', + reply_to=event.message.id, voice_note=True) + if event.raw_text.lower() == 'anytime': + await client.send_file(chat, 'anytime.png', + reply_to=event.message.id) + if '.shrug' in event.text: + await event.edit(event.text.replace('.shrug', r'¯\_(ツ)_/¯')) + + if __name__ == '__main__': - # TG_API_ID and TG_API_HASH *must* exist or this won't run! - session_name = environ.get('TG_SESSION', 'session') - client = TelegramClient( - session_name, int(environ['TG_API_ID']), environ['TG_API_HASH'], - spawn_read_thread=False, proxy=None, update_workers=4 - ) - - @client.on(events.NewMessage) - def my_handler(event: events.NewMessage.Event): - global recent_reacts - - # This utils function gets the unique identifier from peers (to_id) - to_id = utils.get_peer_id(event.message.to_id) - - # Through event.raw_text we access the text of messages without format - words = re.split('\W+', event.raw_text) - - # Try to match some reaction - for trigger, response in REACTS.items(): - if len(recent_reacts[to_id]) > 3: - # Silently ignore triggers if we've recently sent 3 reactions - break - - if trigger in words: - # Remove recent replies older than 10 minutes - recent_reacts[to_id] = [ - a for a in recent_reacts[to_id] if - datetime.now() - a < timedelta(minutes=10) - ] - # Send a reaction as a reply (otherwise, event.respond()) - event.reply(response) - # Add this reaction to the list of recent actions - recent_reacts[to_id].append(datetime.now()) - - # Automatically send relevant media when we say certain things - # When invoking requests, get_input_entity needs to be called manually - if event.out: - if event.raw_text.lower() == 'x files theme': - client.send_voice_note(event.message.to_id, 'xfiles.m4a', - reply_to=event.message.id) - if event.raw_text.lower() == 'anytime': - client.send_file(event.message.to_id, 'anytime.png', - reply_to=event.message.id) - if '.shrug' in event.text: - event.edit(event.text.replace('.shrug', r'¯\_(ツ)_/¯')) - - if 'TG_PHONE' in environ: - client.start(phone=environ['TG_PHONE']) - else: - client.start() - + loop = asyncio.get_event_loop() + loop.run_until_complete(client.start(phone=environ.get('TG_PHONE'))) print('(Press Ctrl+C to stop this)') - client.idle() + client.run_until_disconnected() diff --git a/try_telethon.py b/try_telethon.py index 74eb08c0..e0bf2c8a 100755 --- a/try_telethon.py +++ b/try_telethon.py @@ -1,4 +1,5 @@ #!/usr/bin/env python3 +import asyncio import traceback from telethon_examples.interactive_telegram_client \ @@ -38,14 +39,15 @@ if __name__ == '__main__': **kwargs) print('Initialization done!') + loop = asyncio.get_event_loop() try: - client.run() + loop.run_until_complete(client.run()) except Exception as e: print('Unexpected error ({}): {} at\n{}'.format( type(e), e, traceback.format_exc())) finally: - client.disconnect() + loop.run_until_complete(client.disconnect()) print('Thanks for trying the interactive example! Exiting...')