diff --git a/readthedocs/quick-references/faq.rst b/readthedocs/quick-references/faq.rst
index 70ab9bfa..f452b9ea 100644
--- a/readthedocs/quick-references/faq.rst
+++ b/readthedocs/quick-references/faq.rst
@@ -213,6 +213,9 @@ lot of headaches to get threads and asyncio to work together. Instead,
consider using `Quart `_, an asyncio-based
alternative to `Flask `_.
+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
diff --git a/telethon_examples/README.md b/telethon_examples/README.md
index 3cab7322..29af0e4d 100644
--- a/telethon_examples/README.md
+++ b/telethon_examples/README.md
@@ -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**.
@@ -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
diff --git a/telethon_examples/quart_login.py b/telethon_examples/quart_login.py
new file mode 100644
index 00000000..b4715c66
--- /dev/null
+++ b/telethon_examples/quart_login.py
@@ -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 '''
+
+
+
+
+ Telethon + Quart
+
+ {}
+
+'''.format(inner)
+
+
+# Helper method to format messages nicely
+async def format_message(message):
+ if message.photo:
+ content = ''.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', '
')
+
+ return '{}: {}{}
'.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 = '{}
'.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('''
+''')
+
+ # We have the phone, but we're not logged in, so ask for the code
+ return html('''
+''')
+
+
+# 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