Repository: aiogram/aiogram
Pull request #1254 (2023-08-10 07:56:30)

Examples/dev 3x multi file
# Description

This update includes the addition of a multi-file bot example to the repository. The purpose of this change is to provide more varied and comprehensive examples for users learning to implement bots using our codebase.

Fixes # (issue)

## Type of change

Please delete options that are not relevant.

- [x] Documentation (typos, code examples or any documentation update)
- [ ] Bug fix (non-breaking change which fixes an issue)
- [ ] New feature (non-breaking change which adds functionality)
- [ ] Breaking change (fix or feature that would cause existing functionality to not work as expected)
- [ ] This change requires a documentation update

# How Has This Been Tested?

Please describe the tests that you ran to verify your changes. Provide instructions so we can reproduce. Please also list any relevant details for your test configuration.

- [x] Ran locally on my machine and tested the bot.

# Checklist:

- [x] My code follows the style guidelines of this project
- [x] I have performed a self-review of my own code
- [ ] I have made corresponding changes to the documentation
- [x] My changes generate no new warnings
- [ ] I have added tests that prove my fix is effective or that my feature works
- [x] New and existing unit tests pass locally with my changes


github-actions[bot]: # :x: Changelog is required!

You need to add a brief description of the changes to the `CHANGES` directory.

For example, you can run `towncrier create <issue>.<type>` to create a file in the change directory and then write a description on that file.

Read more at [Towncrier docs](https://towncrier.readthedocs.io/en/latest/quickstart.html#creating-news-fragments)

Patch:
diff --git a/CHANGES/1253.doc.rst b/CHANGES/1253.doc.rst
new file mode 100644
index 00000000..d8100687
--- /dev/null
+++ b/CHANGES/1253.doc.rst
@@ -0,0 +1,1 @@
+Added a section on Dependency Injection technology
diff --git a/CHANGES/1254.doc.rst b/CHANGES/1254.doc.rst
new file mode 100644
index 00000000..ce216fa4
--- /dev/null
+++ b/CHANGES/1254.doc.rst
@@ -0,0 +1,1 @@
+This update includes the addition of a multi-file bot example to the repository. 
diff --git a/CHANGES/1256.doc.rst b/CHANGES/1256.doc.rst
new file mode 100644
index 00000000..2b8af247
--- /dev/null
+++ b/CHANGES/1256.doc.rst
@@ -0,0 +1,1 @@
+Refactored examples code to use aiogram enumerations and enhanced chat messages with markdown beautifications for a more user-friendly display.
diff --git a/CHANGES/1259.bugfix.rst b/CHANGES/1259.bugfix.rst
new file mode 100644
index 00000000..e070549d
--- /dev/null
+++ b/CHANGES/1259.bugfix.rst
@@ -0,0 +1,1 @@
+Fixed nested hashtag, cashtag and email message entities not being parsed correctly when these entities are inside another entity.
diff --git a/CHANGES/5780.doc.rst b/CHANGES/5780.doc.rst
new file mode 100644
index 00000000..2b8af247
--- /dev/null
+++ b/CHANGES/5780.doc.rst
@@ -0,0 +1,1 @@
+Refactored examples code to use aiogram enumerations and enhanced chat messages with markdown beautifications for a more user-friendly display.
diff --git a/aiogram/utils/text_decorations.py b/aiogram/utils/text_decorations.py
index 97430a32..eaff6f2c 100644
--- a/aiogram/utils/text_decorations.py
+++ b/aiogram/utils/text_decorations.py
@@ -43,8 +43,11 @@ class TextDecoration(ABC):
             MessageEntityType.URL,
             MessageEntityType.MENTION,
             MessageEntityType.PHONE_NUMBER,
+            MessageEntityType.HASHTAG,
+            MessageEntityType.CASHTAG,
+            MessageEntityType.EMAIL,
         }:
-            # This entities should not be changed
+            # These entities should not be changed
             return text
         if entity.type in {
             MessageEntityType.BOLD,
@@ -71,6 +74,8 @@ class TextDecoration(ABC):
         if entity.type == MessageEntityType.CUSTOM_EMOJI:
             return self.custom_emoji(value=text, custom_emoji_id=cast(str, entity.custom_emoji_id))
 
+        # This case is not possible because of `if` above, but if any new entity is added to
+        # API it will be here too
         return self.quote(text)
 
     def unparse(self, text: str, entities: Optional[List[MessageEntity]] = None) -> str:
diff --git a/docs/dispatcher/dependency_injection.rst b/docs/dispatcher/dependency_injection.rst
new file mode 100644
index 00000000..15b7c908
--- /dev/null
+++ b/docs/dispatcher/dependency_injection.rst
@@ -0,0 +1,56 @@
+####################
+Dependency injection
+####################
+
+Dependency injection is a programming technique that makes a class independent of its dependencies. It achieves that by decoupling the usage of an object from its creation. This helps you to follow `SOLID's <https://en.wikipedia.org/wiki/SOLID>`_ dependency inversion and single responsibility principles.
+
+
+How it works in aiogram
+=======================
+For each update :class:`Dispatcher` passes handling context data. Filters and middleware can also make changes to the context.
+
+To access contextual data you should specify corresponding keyword parameter in handler or filter. For example, to get :class:`FSMContext` we do it like that:
+
+.. code-block:: python
+    @router.message(ProfileCompletion.add_photo, F.photo)
+    async def add_photo(
+        message: types.Message, bot: Bot, state: FSMContext
+    ) -> Any:
+        ... # do something with photo
+
+
+Injecting own dependencies
+==========================
+
+Aiogram provides several ways to complement / modify contextual data.
+
+The first and easiest way is to simply specify the named arguments in :class:`Dispatcher` initialization, polling start methods or :class:`SimpleRequestHandler` initialization if you use webhooks.
+
+.. code-block:: python
+    async def main() -> None:
+        dp = Dispatcher(..., foo=42)
+        return await dp.start_polling(
+            bot, allowed_updates=dp.resolve_used_update_types(), bar="Bazz"
+        )
+Analogy for webhook:
+
+.. code-block:: python
+    async def main() -> None:
+        dp = Dispatcher(..., foo=42)
+        handler = SimpleRequestHandler(dispatcher=dp, bot=bot, bar="Bazz")
+        ... # starting webhook
+:class:`Dispatcher`'s workflow data also can be supplemented by setting values as in a dictionary:
+
+.. code-block:: python
+    dp = Dispatcher(...)
+    dp["eggs"] = Spam()
+The middlewares updates the context quite often.
+You can read more about them on this page:
+
+- `Middlewares <middlewares.html>`__
+
+The last way is to return a dictionary from the filter:
+
+.. literalinclude:: ../../../examples/context_addition_from_filter.py
+
+...or using MagicFilter with :code:`as_()` method. (`Read more <filters/magic_filters.html#get-filter-result-as-handler-argument>`__)
diff --git a/examples/context_addition_from_filter.py b/examples/context_addition_from_filter.py
new file mode 100644
index 00000000..e4eca517
--- /dev/null
+++ b/examples/context_addition_from_filter.py
@@ -0,0 +1,33 @@
+from typing import Any, Dict, Optional, Union
+
+from aiogram import Router
+from aiogram.filters import Filter
+from aiogram.types import Message, User
+
+
+router = Router(name=__name__)
+
+
+class HelloFilter(Filter):
+    def __init__(self, name: Optional[str] = None) -> None:
+        self.name = name
+
+    async def __call__(
+        self,
+        message: Message,
+        event_from_user: User
+        # Filters also can accept keyword parameters like in handlers
+    ) -> Union[bool, Dict[str, Any]]:
+        if message.text.casefold() == "hello":
+            # Returning a dictionary that will update the context data
+            return {"name": event_from_user.mention_html(name=self.name)}
+        return False
+
+
+@router.message(HelloFilter())
+async def my_handler(
+    message: Message, name: str  # Now we can accept "name" as named parameter
+) -> Any:
+    return message.answer(
+        "Hello, {name}!".format(name=name)
+    )
diff --git a/examples/echo_bot.py b/examples/echo_bot.py
index 8ac45e43..fc0bb0ff 100644
--- a/examples/echo_bot.py
+++ b/examples/echo_bot.py
@@ -1,19 +1,22 @@
 import asyncio
 import logging
+import sys
+from os import getenv
 
 from aiogram import Bot, Dispatcher, Router, types
 from aiogram.enums import ParseMode
-from aiogram.filters import Command
+from aiogram.filters import CommandStart
 from aiogram.types import Message
+from aiogram.utils.markdown import hbold
 
 # Bot token can be obtained via https://t.me/BotFather
-TOKEN = "42:TOKEN"
+TOKEN = getenv("BOT_TOKEN")
 
 # All handlers should be attached to the Router (or Dispatcher)
 router = Router()
 
 
-@router.message(Command("start"))
+@router.message(CommandStart())
 async def command_start_handler(message: Message) -> None:
     """
     This handler receives messages with `/start` command
@@ -23,7 +26,7 @@ async def command_start_handler(message: Message) -> None:
     # and the target chat will be passed to :ref:`aiogram.methods.send_message.SendMessage`
     # method automatically or call API method directly via
     # Bot instance: `bot.send_message(chat_id=message.chat.id, ...)`
-    await message.answer(f"Hello, <b>{message.from_user.full_name}!</b>")
+    await message.answer(f"Hello, {hbold(message.from_user.full_name)}!")
 
 
 @router.message()
@@ -54,5 +57,8 @@ async def main() -> None:
 
 
 if __name__ == "__main__":
-    logging.basicConfig(level=logging.INFO)
-    asyncio.run(main())
+    logging.basicConfig(level=logging.INFO, stream=sys.stdout)
+    try:
+        asyncio.run(main())
+    except (KeyboardInterrupt, SystemExit):
+        logging.info("Bot stopped!")
diff --git a/examples/echo_bot_webhook.py b/examples/echo_bot_webhook.py
index d8ed41c5..1fa56c3d 100644
--- a/examples/echo_bot_webhook.py
+++ b/examples/echo_bot_webhook.py
@@ -2,17 +2,20 @@
 This example shows how to use webhook on behind of any reverse proxy (nginx, traefik, ingress etc.)
 """
 import logging
+import sys
+from os import getenv
 
 from aiohttp import web
 
 from aiogram import Bot, Dispatcher, Router, types
 from aiogram.enums import ParseMode
-from aiogram.filters import Command
+from aiogram.filters import CommandStart
 from aiogram.types import Message
+from aiogram.utils.markdown import hbold
 from aiogram.webhook.aiohttp_server import SimpleRequestHandler, setup_application
 
 # Bot token can be obtained via https://t.me/BotFather
-TOKEN = "42:TOKEN"
+TOKEN = getenv("BOT_TOKEN")
 
 # Webserver settings
 # bind localhost only to prevent any external access
@@ -32,7 +35,7 @@ BASE_WEBHOOK_URL = "https://aiogram.dev/"
 router = Router()
 
 
-@router.message(Command(commands=["start"]))
+@router.message(CommandStart())
 async def command_start_handler(message: Message) -> None:
     """
     This handler receives messages with `/start` command
@@ -42,7 +45,7 @@ async def command_start_handler(message: Message) -> None:
     # and the target chat will be passed to :ref:`aiogram.methods.send_message.SendMessage`
     # method automatically or call API method directly via
     # Bot instance: `bot.send_message(chat_id=message.chat.id, ...)`
-    await message.answer(f"Hello, <b>{message.from_user.full_name}!</b>")
+    await message.answer(f"Hello, {hbold(message.from_user.full_name)}!")
 
 
 @router.message()
@@ -63,7 +66,7 @@ async def echo_handler(message: types.Message) -> None:
 async def on_startup(bot: Bot) -> None:
     # If you have a self-signed SSL certificate, then you will need to send a public
     # certificate to Telegram
-    await bot.set_webhook(f"{BASE_WEBHOOK_URL}{WEBHOOK_PATH}")
+    await bot.set_webhook(f"{BASE_WEBHOOK_URL}{WEBHOOK_PATH}", secret_token=WEBHOOK_SECRET)
 
 
 def main() -> None:
@@ -100,5 +103,5 @@ def main() -> None:
 
 
 if __name__ == "__main__":
-    logging.basicConfig(level=logging.INFO)
+    logging.basicConfig(level=logging.INFO, stream=sys.stdout)
     main()
diff --git a/examples/echo_bot_webhook_ssl.py b/examples/echo_bot_webhook_ssl.py
index 463717f0..ad41bc4d 100644
--- a/examples/echo_bot_webhook_ssl.py
+++ b/examples/echo_bot_webhook_ssl.py
@@ -3,17 +3,20 @@ This example shows how to use webhook with SSL certificate.
 """
 import logging
 import ssl
+import sys
+from os import getenv
 
 from aiohttp import web
 
 from aiogram import Bot, Dispatcher, Router, types
 from aiogram.enums import ParseMode
-from aiogram.filters import Command
+from aiogram.filters import CommandStart
 from aiogram.types import FSInputFile, Message
+from aiogram.utils.markdown import hbold
 from aiogram.webhook.aiohttp_server import SimpleRequestHandler, setup_application
 
 # Bot token can be obtained via https://t.me/BotFather
-TOKEN = "42:TOKEN"
+TOKEN = getenv("BOT_TOKEN")
 
 # Webserver settings
 # bind localhost only to prevent any external access
@@ -37,7 +40,7 @@ WEBHOOK_SSL_PRIV = "/path/to/private.key"
 router = Router()
 
 
-@router.message(Command("start"))
+@router.message(CommandStart())
 async def command_start_handler(message: Message) -> None:
     """
     This handler receives messages with `/start` command
@@ -47,7 +50,7 @@ async def command_start_handler(message: Message) -> None:
     # and the target chat will be passed to :ref:`aiogram.methods.send_message.SendMessage`
     # method automatically or call API method directly via
     # Bot instance: `bot.send_message(chat_id=message.chat.id, ...)`
-    await message.answer(f"Hello, <b>{message.from_user.full_name}!</b>")
+    await message.answer(f"Hello, {hbold(message.from_user.full_name)}!")
 
 
 @router.message()
@@ -73,6 +76,7 @@ async def on_startup(bot: Bot) -> None:
     await bot.set_webhook(
         f"{BASE_WEBHOOK_URL}{WEBHOOK_PATH}",
         certificate=FSInputFile(WEBHOOK_SSL_CERT),
+        secret_token=WEBHOOK_SECRET,
     )
 
 
@@ -114,5 +118,5 @@ def main() -> None:
 
 
 if __name__ == "__main__":
-    logging.basicConfig(level=logging.INFO)
+    logging.basicConfig(level=logging.INFO, stream=sys.stdout)
     main()
diff --git a/examples/finite_state_machine.py b/examples/finite_state_machine.py
index 8539015c..260fdfdf 100644
--- a/examples/finite_state_machine.py
+++ b/examples/finite_state_machine.py
@@ -5,7 +5,8 @@ from os import getenv
 from typing import Any, Dict
 
 from aiogram import Bot, Dispatcher, F, Router, html
-from aiogram.filters import Command
+from aiogram.enums import ParseMode
+from aiogram.filters import Command, CommandStart
 from aiogram.fsm.context import FSMContext
 from aiogram.fsm.state import State, StatesGroup
 from aiogram.types import (
@@ -15,6 +16,8 @@ from aiogram.types import (
     ReplyKeyboardRemove,
 )
 
+TOKEN = getenv("BOT_TOKEN")
+
 form_router = Router()
 
 
@@ -24,7 +27,7 @@ class Form(StatesGroup):
     language = State()
 
 
-@form_router.message(Command("start"))
+@form_router.message(CommandStart())
 async def command_start(message: Message, state: FSMContext) -> None:
     await state.set_state(Form.name)
     await message.answer(
@@ -91,7 +94,7 @@ async def process_like_write_bots(message: Message, state: FSMContext) -> None:
 
 
 @form_router.message(Form.like_bots)
-async def process_unknown_write_bots(message: Message, state: FSMContext) -> None:
+async def process_unknown_write_bots(message: Message) -> None:
     await message.reply("I don't understand you :(")
 
 
@@ -99,12 +102,12 @@ async def process_unknown_write_bots(message: Message, state: FSMContext) -> Non
 async def process_language(message: Message, state: FSMContext) -> None:
     data = await state.update_data(language=message.text)
     await state.clear()
-    text = (
-        "Thank for all! Python is in my hearth!\nSee you soon."
-        if message.text.casefold() == "python"
-        else "Thank for information!\nSee you soon."
-    )
-    await message.answer(text)
+
+    if message.text.casefold() == "python":
+        await message.reply(
+            "Python, you say? That's the language that makes my circuits light up! 😉"
+        )
+
     await show_summary(message=message, data=data)
 
 
@@ -121,7 +124,7 @@ async def show_summary(message: Message, data: Dict[str, Any], positive: bool =
 
 
 async def main():
-    bot = Bot(token=getenv("TELEGRAM_TOKEN"), parse_mode="HTML")
+    bot = Bot(token=TOKEN, parse_mode=ParseMode.HTML)
     dp = Dispatcher()
     dp.include_router(form_router)
 
@@ -130,4 +133,7 @@ async def main():
 
 if __name__ == "__main__":
     logging.basicConfig(level=logging.INFO, stream=sys.stdout)
-    asyncio.run(main())
+    try:
+        asyncio.run(main())
+    except (KeyboardInterrupt, SystemExit):
+        logging.info("Bot stopped!")
diff --git a/examples/multi_file_bot/bot.py b/examples/multi_file_bot/bot.py
new file mode 100644
index 00000000..927b52a0
--- /dev/null
+++ b/examples/multi_file_bot/bot.py
@@ -0,0 +1,32 @@
+import asyncio
+import logging
+from os import getenv
+
+from handlers.echo import echo_router
+from handlers.start import start_router
+
+from aiogram import Bot, Dispatcher
+from aiogram.enums import ParseMode
+
+# Bot token can be obtained via https://t.me/BotFather
+TOKEN = getenv("BOT_TOKEN")
+
+
+async def main() -> None:
+    # Dispatcher is a root router
+    dp = Dispatcher()
+    # Register all the routers from handlers package
+    dp.include_routers(
+        start_router,
+        echo_router,
+    )
+
+    # Initialize Bot instance with a default parse mode which will be passed to all API calls
+    bot = Bot(TOKEN, parse_mode=ParseMode.HTML)
+    # And the run events dispatching
+    await dp.start_polling(bot)
+
+
+if __name__ == "__main__":
+    logging.basicConfig(level=logging.INFO)
+    asyncio.run(main())
diff --git a/examples/multi_file_bot/handlers/__init__.py b/examples/multi_file_bot/handlers/__init__.py
new file mode 100644
index 00000000..e69de29b
diff --git a/examples/multi_file_bot/handlers/echo.py b/examples/multi_file_bot/handlers/echo.py
new file mode 100644
index 00000000..4961262c
--- /dev/null
+++ b/examples/multi_file_bot/handlers/echo.py
@@ -0,0 +1,21 @@
+from aiogram import Router
+from aiogram.filters import Command
+from aiogram.types import Message
+
+# For each module with handlers we can create a separate router.
+echo_router = Router()
+
+
+@echo_router.message()
+async def echo_handler(message: Message) -> None:
+    """
+    Handler will forward receive a message back to the sender
+
+    By default, message handler will handle all message types (like a text, photo, sticker etc.)
+    """
+    try:
+        # Send a copy of the received message
+        await message.send_copy(chat_id=message.chat.id)
+    except TypeError:
+        # But not all the types is supported to be copied so need to handle it
+        await message.answer("Nice try!")
diff --git a/examples/multi_file_bot/handlers/start.py b/examples/multi_file_bot/handlers/start.py
new file mode 100644
index 00000000..804268c3
--- /dev/null
+++ b/examples/multi_file_bot/handlers/start.py
@@ -0,0 +1,18 @@
+from aiogram import Router
+from aiogram.filters import Command
+from aiogram.types import Message
+
+start_router = Router()
+
+
+@start_router.message(Command("start"))
+async def command_start_handler(message: Message) -> None:
+    """
+    This handler receives messages with `/start` command
+    """
+    # Most event objects have aliases for API methods that can be called in events' context
+    # For example if you want to answer to incoming message you can use `message.answer(...)` alias
+    # and the target chat will be passed to :ref:`aiogram.methods.send_message.SendMessage`
+    # method automatically or call API method directly via
+    # Bot instance: `bot.send_message(chat_id=message.chat.id, ...)`
+    await message.answer(f"Hello, <b>{message.from_user.full_name}!</b>")
diff --git a/examples/multibot.py b/examples/multibot.py
index 57e75f8e..82fac4a1 100644
--- a/examples/multibot.py
+++ b/examples/multibot.py
@@ -1,14 +1,16 @@
+import logging
+import sys
 from os import getenv
 from typing import Any, Dict, Union
 
 from aiohttp import web
-from finite_state_machine import form_router
 
 from aiogram import Bot, Dispatcher, F, Router
 from aiogram.client.session.aiohttp import AiohttpSession
+from aiogram.enums import ParseMode
 from aiogram.exceptions import TelegramUnauthorizedError
 from aiogram.filters import Command, CommandObject
-from aiogram.fsm.storage.redis import DefaultKeyBuilder, RedisStorage
+from aiogram.fsm.storage.memory import MemoryStorage
 from aiogram.types import Message
 from aiogram.utils.token import TokenValidationError, validate_token
 from aiogram.webhook.aiohttp_server import (
@@ -16,11 +18,12 @@ from aiogram.webhook.aiohttp_server import (
     TokenBasedRequestHandler,
     setup_application,
 )
+from finite_state_machine import form_router
 
 main_router = Router()
 
 BASE_URL = getenv("BASE_URL", "https://example.com")
-MAIN_BOT_TOKEN = getenv("TELEGRAM_TOKEN")
+MAIN_BOT_TOKEN = getenv("BOT_TOKEN")
 
 WEB_SERVER_HOST = "127.0.0.1"
 WEB_SERVER_PORT = 8080
@@ -56,10 +59,13 @@ async def on_startup(dispatcher: Dispatcher, bot: Bot):
 
 
 def main():
+    logging.basicConfig(level=logging.INFO, stream=sys.stdout)
     session = AiohttpSession()
-    bot_settings = {"session": session, "parse_mode": "HTML"}
+    bot_settings = {"session": session, "parse_mode": ParseMode.HTML}
     bot = Bot(token=MAIN_BOT_TOKEN, **bot_settings)
-    storage = RedisStorage.from_url(REDIS_DSN, key_builder=DefaultKeyBuilder(with_bot_id=True))
+    storage = MemoryStorage()
+    # In order to use RedisStorage you need to use Key Builder with bot ID:
+    # storage = RedisStorage.from_url(REDIS_DSN, key_builder=DefaultKeyBuilder(with_bot_id=True))
 
     main_dispatcher = Dispatcher(storage=storage)
     main_dispatcher.include_router(main_router)
diff --git a/examples/specify_updates.py b/examples/specify_updates.py
index b5d22afc..ead296ea 100644
--- a/examples/specify_updates.py
+++ b/examples/specify_updates.py
@@ -1,8 +1,11 @@
 import asyncio
 import logging
+import sys
+from os import getenv
 
 from aiogram import Bot, Dispatcher, Router
-from aiogram.filters import Command
+from aiogram.enums import ParseMode
+from aiogram.filters import LEAVE_TRANSITION, ChatMemberUpdatedFilter, CommandStart
 from aiogram.types import (
     CallbackQuery,
     ChatMemberUpdated,
@@ -10,8 +13,9 @@ from aiogram.types import (
     InlineKeyboardMarkup,
     Message,
 )
+from aiogram.utils.markdown import hbold, hcode
 
-TOKEN = "6wo"
+TOKEN = getenv("BOT_TOKEN")
 
 logger = logging.getLogger(__name__)
 logging.basicConfig(level=logging.INFO)
@@ -19,14 +23,14 @@ logging.basicConfig(level=logging.INFO)
 router = Router()
 
 
-@router.message(Command("start"))
+@router.message(CommandStart())
 async def command_start_handler(message: Message) -> None:
     """
     This handler receives messages with `/start` command
     """
 
     await message.answer(
-        f"Hello, <b>{message.from_user.full_name}!</b>",
+        f"Hello, {hbold(message.from_user.full_name)}!",
         reply_markup=InlineKeyboardMarkup(
             inline_keyboard=[[InlineKeyboardButton(text="Tap me, bro", callback_data="*")]]
         ),
@@ -37,7 +41,7 @@ async def command_start_handler(message: Message) -> None:
 async def chat_member_update(chat_member: ChatMemberUpdated, bot: Bot) -> None:
     await bot.send_message(
         chat_member.chat.id,
-        "Member {chat_member.from_user.id} was changed "
+        f"Member {hcode(chat_member.from_user.id)} was changed "
         + f"from {chat_member.old_chat_member.status} to {chat_member.new_chat_member.status}",
     )
 
@@ -48,7 +52,7 @@ sub_router = Router()
 
 @sub_router.callback_query()
 async def callback_tap_me(callback_query: CallbackQuery) -> None:
-    await callback_query.answer("Yeah good, now i'm fine")
+    await callback_query.answer("Yeah good, now I'm fine")
 
 
 # this router will use only edited_message updates
@@ -57,38 +61,39 @@ sub_sub_router = Router()
 
 @sub_sub_router.edited_message()
 async def edited_message_handler(edited_message: Message) -> None:
-    await edited_message.reply("Message was edited, big brother watch you")
+    await edited_message.reply("Message was edited, Big Brother watches you")
 
 
 # this router will use only my_chat_member updates
 deep_dark_router = Router()
 
 
-@deep_dark_router.my_chat_member()
+@deep_dark_router.my_chat_member(~ChatMemberUpdatedFilter(~LEAVE_TRANSITION))
 async def my_chat_member_change(chat_member: ChatMemberUpdated, bot: Bot) -> None:
     await bot.send_message(
         chat_member.chat.id,
-        "Member was changed from "
-        + f"{chat_member.old_chat_member.status} to {chat_member.new_chat_member.status}",
+        f"This Bot`s status was changed from {hbold(chat_member.old_chat_member.status)} "
+        f"to {hbold(chat_member.new_chat_member.status)}",
     )
 
 
 async def main() -> None:
     # Initialize Bot instance with a default parse mode which will be passed to all API calls
-    bot = Bot(TOKEN, parse_mode="HTML")
+    bot = Bot(TOKEN, parse_mode=ParseMode.HTML)
 
     dp = Dispatcher()
-    dp.include_router(router)
-    sub_router.include_router(deep_dark_router)
-    router.include_router(sub_router)
-    router.include_router(sub_sub_router)
 
-    useful_updates = dp.resolve_used_update_types()
+    sub_router.include_router(deep_dark_router)
+    router.include_routers(sub_router, sub_sub_router)
+    dp.include_router(router)
 
-    # And the run events dispatching
-    await dp.start_polling(bot, allowed_updates=useful_updates)
+    # Start event dispatching
+    await dp.start_polling(bot)
 
 
 if __name__ == "__main__":
-    logging.basicConfig(level=logging.INFO)
-    asyncio.run(main())
+    logging.basicConfig(level=logging.INFO, stream=sys.stdout)
+    try:
+        asyncio.run(main())
+    except (KeyboardInterrupt, SystemExit):
+        logger.info("Bot stopped!")
diff --git a/examples/web_app/main.py b/examples/web_app/main.py
index 0d58042c..db148153 100644
--- a/examples/web_app/main.py
+++ b/examples/web_app/main.py
@@ -1,4 +1,5 @@
 import logging
+import sys
 from os import getenv
 
 from aiohttp.web import run_app
@@ -10,7 +11,8 @@ from aiogram import Bot, Dispatcher
 from aiogram.types import MenuButtonWebApp, WebAppInfo
 from aiogram.webhook.aiohttp_server import SimpleRequestHandler, setup_application
 
-TELEGRAM_TOKEN = getenv("TELEGRAM_TOKEN")
+TOKEN = getenv("BOT_TOKEN")
+
 APP_BASE_URL = getenv("APP_BASE_URL")
 
 
@@ -22,7 +24,7 @@ async def on_startup(bot: Bot, base_url: str):
 
 
 def main():
-    bot = Bot(token=TELEGRAM_TOKEN, parse_mode="HTML")
+    bot = Bot(token=TOKEN, parse_mode="HTML")
     dispatcher = Dispatcher()
     dispatcher["base_url"] = APP_BASE_URL
     dispatcher.startup.register(on_startup)
@@ -45,5 +47,5 @@ def main():
 
 
 if __name__ == "__main__":
-    logging.basicConfig(level=logging.INFO)
+    logging.basicConfig(level=logging.INFO, stream=sys.stdout)
     main()


Test patch:
diff --git a/tests/test_utils/test_text_decorations.py b/tests/test_utils/test_text_decorations.py
index 056bd1cb..25f222f0 100644
--- a/tests/test_utils/test_text_decorations.py
+++ b/tests/test_utils/test_text_decorations.py
@@ -113,6 +113,14 @@ class TestTextDecoration:
     ):
         assert decorator.apply_entity(entity, "test") == result
 
+    def test_unknown_apply_entity(self):
+        assert (
+            html_decoration.apply_entity(
+                MessageEntity(type="unknown", offset=0, length=5), "<test>"
+            )
+            == "&lt;test&gt;"
+        )
+
     @pytest.mark.parametrize(
         "decorator,before,after",
         [
@@ -243,6 +251,33 @@ class TestTextDecoration:
                 [MessageEntity(type="bold", offset=0, length=8, url=None, user=None)],
                 "<b>👋🏾 Hi!</b>",
             ],
+            [
+                html_decoration,
+                "#test",
+                [
+                    MessageEntity(type="hashtag", offset=0, length=5),
+                    MessageEntity(type="bold", offset=0, length=5),
+                ],
+                "<b>#test</b>",
+            ],
+            [
+                html_decoration,
+                "$TEST",
+                [
+                    MessageEntity(type="cashtag", offset=0, length=5),
+                    MessageEntity(type="bold", offset=0, length=5),
+                ],
+                "<b>$TEST</b>",
+            ],
+            [
+                html_decoration,
+                "test@example.com",
+                [
+                    MessageEntity(type="email", offset=0, length=16),
+                    MessageEntity(type="bold", offset=0, length=16),
+                ],
+                "<b>test@example.com</b>",
+            ],
         ],
     )
     def test_unparse(