Improve Quart example

As the client seems tied to a specific login, I've changed it from
being a global to a local in the request handler. This should prevent
one user logging in and another unrelated user having access (due to a
global shared client). I've then made use of session storage to store
the phone and code (session storage is per user and deleted when the
browser tab is closed). I wasn't sure if the code was sensitive, if so
it is potentially problematic to store in the default session system
(cookies) - a server storage solution (e.g. redis or in memory) would
be better.

Note that if the client has to be global the Quart before/after
serving functions should be used to connect and disconnect, as so,

    client = ...

    @app.before_serving
    async def startup():
        await client.connect()

    @app.after_serving
    async def cleanup():
        await client.disconnect()

as this creates the client within the event-loop managed by
Quart. Alternatively if the event-loop must be managed outside of
Quart the following should be used,

    from hypercorn.asyncio import serve

    loop.run_until_complete(serve(app))

instead of `app.run()`.

I've then made a number of more minor changes to use the template
rendering system within Quart (more features, better example) and
to clean up the code.
This commit is contained in:
pgjones 2019-07-09 20:05:17 +01:00
parent c4c263a85b
commit c61a5c8fc6

View File

@ -1,10 +1,38 @@
import base64
import os
from quart import Quart, request
from quart import Quart, render_template_string, request, session
from telethon import TelegramClient, utils
BASE_TEMPLATE = """
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>Telethon + Quart</title>
</head>
<body>{{ content | safe }}</body>
</html>
"""
PHONE_FORM = """
<form action="/" method="post">
Phone (international format): <input name="phone" type="text" placeholder="+34600000000">
<input type="submit">
</form>
"""
CODE_FORM = """
<form action="/" method="post">
Telegram code: <input name="code" type="text" placeholder="70707">
<input type="submit">
</form>
"""
app = Quart(__name__)
app.secret_key = "CHANGE THIS TO SOMETHING SECRET"
def get_env(name, message):
if name in os.environ:
@ -18,20 +46,6 @@ API_ID = int(get_env('TG_API_ID', 'Enter your API ID: '))
API_HASH = get_env('TG_API_HASH', 'Enter your API hash: ')
# Helper method to add the HTML head/body
def html(inner):
return '''
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>Telethon + Quart</title>
</head>
<body>{}</body>
</html>
'''.format(inner)
# Helper method to format messages nicely
async def format_message(message):
if message.photo:
@ -50,71 +64,36 @@ async def format_message(message):
)
# Define the global phone and Quart app variables
phone = None
app = Quart(__name__)
# Quart handlers
@app.route('/', methods=['GET', 'POST'])
async def root():
# Connect if we aren't yet
if not client.is_connected():
await client.connect()
client = TelegramClient(SESSION, API_ID, API_HASH)
await client.connect()
# We want to update the global phone variable to remember it
global phone
if request.method == "POST":
form = await request.form
if 'phone' in form:
session["phone"] = form["phone"]
await client.send_code_request(form["phone"])
# Check form parameters (phone/code)
form = await request.form
if 'phone' in form:
phone = form['phone']
await client.send_code_request(phone)
if 'code' in form:
session["code"] = form["code"]
if 'code' in form:
await client.sign_in(code=form['code'])
if "phone" not in session:
return await render_template_string(BASE_TEMPLATE, content=PHONE_FORM)
# If we're logged in, show them some messages from their first dialog
if await client.is_user_authorized():
# They are logged in, show them some messages from their first dialog
dialog = (await client.get_dialogs())[0]
result = '<h1>{}</h1>'.format(dialog.title)
async for m in client.iter_messages(dialog, 10):
result += await(format_message(m))
if "code" not in session:
return await render_template_string(BASE_TEMPLATE, content=CODE_FORM)
return html(result)
await client.sign_in(code=session["code"])
# They are logged in, show them some messages from their first dialog
dialog = (await client.get_dialogs())[0]
result = '<h1>{}</h1>'.format(dialog.title)
async for m in client.iter_messages(dialog, 10):
result += await(format_message(m))
# Ask for the phone if we don't know it yet
if phone is None:
return html('''
<form action="/" method="post">
Phone (international format): <input name="phone" type="text" placeholder="+34600000000">
<input type="submit">
</form>''')
# We have the phone, but we're not logged in, so ask for the code
return html('''
<form action="/" method="post">
Telegram code: <input name="code" type="text" placeholder="70707">
<input type="submit">
</form>''')
await client.disconnect()
return await render_template_string(BASE_TEMPLATE, content=result)
# By default, `Quart.run` uses `asyncio.run()`, which creates a new asyncio
# event loop. If we create the `TelegramClient` before, `telethon` will
# use `asyncio.get_event_loop()`, which is the implicit loop in the main
# thread. These two loops are different, and it won't work.
#
# So, we have to manually pass the same `loop` to both applications to
# make 100% sure it works and to avoid headaches.
#
# Quart doesn't seem to offer a way to run inside `async def`
# (see https://gitlab.com/pgjones/quart/issues/146) so we must
# run and block on it last.
#
# This example creates a global client outside of Quart handlers.
# If you create the client inside the handlers (common case), you
# won't have to worry about any of this.
client = TelegramClient(SESSION, API_ID, API_HASH)
client.parse_mode = 'html' # <- render things nicely
app.run(loop=client.loop) # <- same event loop as telethon
if __name__ == "__main__":
app.run()