Update examples to be async

This commit is contained in:
Lonami Exo 2018-06-24 12:04:23 +02:00
parent 58031b3adf
commit 026c0c4f9d
5 changed files with 134 additions and 116 deletions

View File

@ -20,7 +20,7 @@ its own methods, which you all can use.
# Now you can use all client methods listed below, like for example... # Now you can use all client methods listed below, like for example...
await client.send_message('me', 'Hello to myself!') 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. You **don't** need to import these `AuthMethods`, `MessageMethods`, etc.

View File

@ -1,4 +1,6 @@
import os import os
import sys
import asyncio
from getpass import getpass from getpass import getpass
from telethon.utils import get_display_name 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 import TelegramClient, events
from telethon.network import ConnectionTcpAbridged from telethon.network import ConnectionTcpAbridged
from telethon.errors import SessionPasswordNeededError 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): 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): class InteractiveTelegramClient(TelegramClient):
"""Full featured Telegram client, meant to be used on an interactive """Full featured Telegram client, meant to be used on an interactive
session to see what Telethon is capable off - session to see what Telethon is capable off -
@ -78,41 +91,37 @@ class InteractiveTelegramClient(TelegramClient):
connection=ConnectionTcpAbridged, connection=ConnectionTcpAbridged,
# If you're using a proxy, set it here. # If you're using a proxy, set it here.
proxy=proxy, 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
) )
# Store {message.id: message} map here so that we can download # Store {message.id: message} map here so that we can download
# media known the message ID, for every message having media. # media known the message ID, for every message having media.
self.found_media = {} self.found_media = {}
# Calling .connect() may return False, so you need to assert it's # Calling .connect() may raise a connection error False, so you need
# True before continuing. Otherwise you may want to retry as done here. # to except those before continuing. Otherwise you may want to retry
# as done here.
print('Connecting to Telegram servers...') print('Connecting to Telegram servers...')
if not self.connect(): try:
loop.run_until_complete(self.connect())
except ConnectionError:
print('Initial connection failed. Retrying...') print('Initial connection failed. Retrying...')
if not self.connect(): loop.run_until_complete(self.connect())
print('Could not connect to Telegram servers.')
return
# If the user hasn't called .sign_in() or .sign_up() yet, they won't # 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 # be authorized. The first thing you must do is authorize. Calling
# .sign_in() should only be done once as the information is saved on # .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. # 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...') print('First run. Sending code request...')
self.sign_in(user_phone) loop.run_until_complete(self.sign_in(user_phone))
self_user = None self_user = None
while self_user is None: while self_user is None:
code = input('Enter the code you just received: ') code = input('Enter the code you just received: ')
try: 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 # Two-step verification may be enabled, and .sign_in will
# raise this error. If that's the case ask for the password. # 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. ' pw = getpass('Two step verification is enabled. '
'Please enter your password: ') '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""" """Main loop of the TelegramClient, will wait for user action"""
# Once everything is ready, we can add an event handler. # Once everything is ready, we can add an event handler.
@ -142,7 +152,7 @@ class InteractiveTelegramClient(TelegramClient):
# Entities represent the user, chat or channel # Entities represent the user, chat or channel
# corresponding to the dialog on the same index. # 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 i = None
while i is None: while i is None:
@ -159,7 +169,7 @@ class InteractiveTelegramClient(TelegramClient):
print(' !q: Quits the dialogs window and exits.') print(' !q: Quits the dialogs window and exits.')
print(' !l: Logs out, terminating this session.') print(' !l: Logs out, terminating this session.')
print() print()
i = input('Enter dialog ID or a command: ') i = await async_input('Enter dialog ID or a command: ')
if i == '!q': if i == '!q':
return return
if i == '!l': if i == '!l':
@ -169,7 +179,7 @@ class InteractiveTelegramClient(TelegramClient):
# #
# This is not the same as simply calling .disconnect(), # This is not the same as simply calling .disconnect(),
# which simply shuts down everything gracefully. # which simply shuts down everything gracefully.
self.log_out() await self.log_out()
return return
try: try:
@ -199,7 +209,7 @@ class InteractiveTelegramClient(TelegramClient):
# And start a while loop to chat # And start a while loop to chat
while True: while True:
msg = input('Enter a message: ') msg = await async_input('Enter a message: ')
# Quit # Quit
if msg == '!q': if msg == '!q':
break break
@ -209,16 +219,16 @@ class InteractiveTelegramClient(TelegramClient):
# History # History
elif msg == '!h': elif msg == '!h':
# First retrieve the messages and some information # 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 # Iterate over all (in reverse order so the latest appear
# the last in the console) and print them with format: # the last in the console) and print them with format:
# "[hh:mm] Sender: Message" # "[hh:mm] Sender: Message"
for msg in reversed(messages): for msg in reversed(messages):
# Note that the .sender attribute is only there for # Note how we access .sender here. Since we made an
# convenience, the API returns it differently. But # API call using the self client, it will always have
# this shouldn't concern us. See the documentation # information about the sender. This is different to
# for .iter_messages() for more information. # events, where Telegram may not always send the user.
name = get_display_name(msg.sender) name = get_display_name(msg.sender)
# Format the message content # Format the message content
@ -242,29 +252,33 @@ class InteractiveTelegramClient(TelegramClient):
# Send photo # Send photo
elif msg.startswith('!up '): elif msg.startswith('!up '):
# Slice the message to get the path # 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) # Send file (document)
elif msg.startswith('!uf '): elif msg.startswith('!uf '):
# Slice the message to get the path # 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 # Delete messages
elif msg.startswith('!d '): elif msg.startswith('!d '):
# Slice the message to get message ID # 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)) print('Deleted {}'.format(deleted_msg))
# Download media # Download media
elif msg.startswith('!dm '): elif msg.startswith('!dm '):
# Slice the message to get message ID # 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 # Download profile photo
elif msg == '!dp': elif msg == '!dp':
print('Downloading profile picture to usermedia/...') print('Downloading profile picture to usermedia/...')
os.makedirs('usermedia', exist_ok=True) os.makedirs('usermedia', exist_ok=True)
output = self.download_profile_photo(entity, 'usermedia') output = await self.download_profile_photo(entity,
'usermedia')
if output: if output:
print('Profile picture downloaded to', output) print('Profile picture downloaded to', output)
else: else:
@ -278,26 +292,26 @@ class InteractiveTelegramClient(TelegramClient):
# Send chat message (if any) # Send chat message (if any)
elif msg: 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""" """Sends the file located at path to the desired entity as a photo"""
self.send_file( await self.send_file(
entity, path, entity, path,
progress_callback=self.upload_progress_callback progress_callback=self.upload_progress_callback
) )
print('Photo sent!') 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""" """Sends the file located at path to the desired entity as a document"""
self.send_file( await self.send_file(
entity, path, entity, path,
force_document=True, force_document=True,
progress_callback=self.upload_progress_callback progress_callback=self.upload_progress_callback
) )
print('Document sent!') 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 """Given a message ID, finds the media this message contained and
downloads it. downloads it.
""" """
@ -310,7 +324,7 @@ class InteractiveTelegramClient(TelegramClient):
print('Downloading media to usermedia/...') print('Downloading media to usermedia/...')
os.makedirs('usermedia', exist_ok=True) os.makedirs('usermedia', exist_ok=True)
output = self.download_media( output = await self.download_media(
msg.media, msg.media,
file='usermedia/', file='usermedia/',
progress_callback=self.download_progress_callback progress_callback=self.download_progress_callback
@ -336,29 +350,33 @@ class InteractiveTelegramClient(TelegramClient):
bytes_to_string(total_bytes), downloaded_bytes / total_bytes) 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""" """Callback method for received events.NewMessage"""
# Note that accessing ``.sender`` and ``.chat`` may be slow since # Note that message_handler is called when a Telegram update occurs
# these are not cached and must be queried always! However it lets # and an event is created. Telegram may not always send information
# us access the chat title and user name. # 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.is_group:
if event.out: if event.out:
sprint('>> sent "{}" to chat {}'.format( sprint('>> sent "{}" to chat {}'.format(
event.text, get_display_name(event.chat) event.text, get_display_name(chat)
)) ))
else: else:
sprint('<< {} @ {} sent "{}"'.format( sprint('<< {} @ {} sent "{}"'.format(
get_display_name(event.sender), get_display_name(await event.get_sender()),
get_display_name(event.chat), get_display_name(chat),
event.text event.text
)) ))
else: else:
if event.out: if event.out:
sprint('>> "{}" to user {}'.format( sprint('>> "{}" to user {}'.format(
event.text, get_display_name(event.chat) event.text, get_display_name(chat)
)) ))
else: else:
sprint('<< {} sent "{}"'.format( sprint('<< {} sent "{}"'.format(
get_display_name(event.chat), event.text get_display_name(chat), event.text
)) ))

View File

@ -5,6 +5,7 @@
# your environment variables. This is a good way to use these private # your environment variables. This is a good way to use these private
# values. See https://superuser.com/q/284342. # values. See https://superuser.com/q/284342.
import asyncio
from os import environ from os import environ
# environ is used to get API information from environment variables # environ is used to get API information from environment variables
@ -13,28 +14,27 @@ from os import environ
from telethon import TelegramClient from telethon import TelegramClient
def main(): async def main():
session_name = environ.get('TG_SESSION', 'session') session_name = environ.get('TG_SESSION', 'session')
client = TelegramClient(session_name, client = TelegramClient(session_name,
int(environ['TG_API_ID']), int(environ['TG_API_ID']),
environ['TG_API_HASH'], environ['TG_API_HASH'],
proxy=None, proxy=None)
update_workers=4,
spawn_read_thread=False)
if 'TG_PHONE' in environ: if 'TG_PHONE' in environ:
client.start(phone=environ['TG_PHONE']) await client.start(phone=environ['TG_PHONE'])
else: else:
client.start() await client.start()
client.add_event_handler(update_handler) client.add_event_handler(update_handler)
print('(Press Ctrl+C to stop this)') print('(Press Ctrl+C to stop this)')
client.idle() await client.disconnected
def update_handler(update): async def update_handler(update):
print(update) print(update)
if __name__ == '__main__': if __name__ == '__main__':
main() loop = asyncio.get_event_loop()
loop.run_until_complete(main())

View File

@ -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. such as "xfiles.m4a" or "anytime.png" for some of the automated replies.
""" """
import re import re
import asyncio
from collections import defaultdict from collections import defaultdict
from datetime import datetime, timedelta from datetime import datetime, timedelta
from os import environ from os import environ
from telethon import TelegramClient, events, utils from telethon import TelegramClient, events
"""Uncomment this for debugging """Uncomment this for debugging
import logging import logging
@ -30,57 +31,54 @@ REACTS = {'emacs': 'Needs more vim',
recent_reacts = defaultdict(list) 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__': if __name__ == '__main__':
# TG_API_ID and TG_API_HASH *must* exist or this won't run! loop = asyncio.get_event_loop()
session_name = environ.get('TG_SESSION', 'session') loop.run_until_complete(client.start(phone=environ.get('TG_PHONE')))
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()
print('(Press Ctrl+C to stop this)') print('(Press Ctrl+C to stop this)')
client.idle() client.run_until_disconnected()

View File

@ -1,4 +1,5 @@
#!/usr/bin/env python3 #!/usr/bin/env python3
import asyncio
import traceback import traceback
from telethon_examples.interactive_telegram_client \ from telethon_examples.interactive_telegram_client \
@ -38,14 +39,15 @@ if __name__ == '__main__':
**kwargs) **kwargs)
print('Initialization done!') print('Initialization done!')
loop = asyncio.get_event_loop()
try: try:
client.run() loop.run_until_complete(client.run())
except Exception as e: except Exception as e:
print('Unexpected error ({}): {} at\n{}'.format( print('Unexpected error ({}): {} at\n{}'.format(
type(e), e, traceback.format_exc())) type(e), e, traceback.format_exc()))
finally: finally:
client.disconnect() loop.run_until_complete(client.disconnect())
print('Thanks for trying the interactive example! Exiting...') print('Thanks for trying the interactive example! Exiting...')