Skip to content

Commit

Permalink
Add quart-based example (#1207)
Browse files Browse the repository at this point in the history
  • Loading branch information
Lonami committed Jun 27, 2019
1 parent 4f1edeb commit 2a7d431
Show file tree
Hide file tree
Showing 3 changed files with 148 additions and 0 deletions.
3 changes: 3 additions & 0 deletions readthedocs/quick-references/faq.rst
Original file line number Diff line number Diff line change
Expand Up @@ -213,6 +213,9 @@ lot of headaches to get threads and asyncio to work together. Instead,
consider using `Quart <https://pgjones.gitlab.io/quart/>`_, an asyncio-based
alternative to `Flask <flask.pocoo.org/>`_.

Check out `quart_login.py`_ for an example web-application based on Quart.

.. _logging: https://docs.python.org/3/library/logging.html
.. _@SpamBot: https://t.me/SpamBot
.. _issue 297: https://github.com/LonamiWebs/Telethon/issues/297
.. _quart_login.py: https://github.com/LonamiWebs/Telethon/tree/master/telethon_examples#quart_loginpy
25 changes: 25 additions & 0 deletions telethon_examples/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,30 @@ send messages, delete them, and download media. The code is a bit
long which may make it harder to follow, and requires saving some
state in order for downloads to work later.

### [`quart_login.py`]

* Usable as: **user**.
* Difficulty: **medium**.

Web-based application using [Quart](https://pgjones.gitlab.io/quart/index.html)
(an `asyncio` alternative to [Flask](http://flask.pocoo.org/)) and Telethon
together.

The example should work as a base for Quart applications *with a single
global client*, and it should be easy to adapt for multiple clients by
following the comments in the code.

It showcases how to login manually (ask for phone, code, and login),
and once the user is logged in, some messages and photos will be shown
in the page.

There is nothing special about Quart. It was chosen because it's a
drop-in replacement for Flask, the most popular option for web-apps.
You can use any `asyncio` library with Telethon just as well,
like [Sanic](https://sanic.readthedocs.io/en/latest/index.html) or
[aiohttp](https://docs.aiohttp.org/en/stable/). You can even use Flask,
if you learn how to use `threading` and `asyncio` together.

### [`gui.py`]

* Usable as: **user and bot**.
Expand All @@ -111,6 +135,7 @@ assumes some [`asyncio`] knowledge, but otherwise is easy to follow.
[CC0 License]: https://github.com/LonamiWebs/Telethon/blob/master/telethon_examples/LICENSE
[@BotFather]: https://t.me/BotFather
[`assistant.py`]: https://raw.githubusercontent.com/LonamiWebs/Telethon/master/telethon_examples/assistant.py
[`quart_login.py`]: https://raw.githubusercontent.com/LonamiWebs/Telethon/master/telethon_examples/quart_login.py
[`gui.py`]: https://raw.githubusercontent.com/LonamiWebs/Telethon/master/telethon_examples/gui.py
[`interactive_telegram_client.py`]: https://raw.githubusercontent.com/LonamiWebs/Telethon/master/telethon_examples/interactive_telegram_client.py
[`print_messages.py`]: https://raw.githubusercontent.com/LonamiWebs/Telethon/master/telethon_examples/print_messages.py
Expand Down
120 changes: 120 additions & 0 deletions telethon_examples/quart_login.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
import base64
import os

from quart import Quart, request

from telethon import TelegramClient, utils


def get_env(name, message):
if name in os.environ:
return os.environ[name]
return input(message)


# Session name, API ID and hash to use; loaded from environmental variables
SESSION = os.environ.get('TG_SESSION', 'quart')
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:
content = '<img src="data:image/png;base64,{}" alt="{}" />'.format(
base64.b64encode(await message.download_media(bytes)).decode(),
message.raw_text
)
else:
# client.parse_mode = 'html', so bold etc. will work!
content = (message.text or '(action message)').replace('\n', '<br>')

return '<p><strong>{}</strong>: {}<sub>{}</sub></p>'.format(
utils.get_display_name(message.sender),
content,
message.date
)


# 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()

# We want to update the global phone variable to remember it
global 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:
await client.sign_in(code=form['code'])

# 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))

return html(result)

# 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>''')


# 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

0 comments on commit 2a7d431

Please sign in to comment.