Repository: aiogram/aiogram
Pull request #1252 (2023-08-08 14:17:49)

Fix missing message content types
# Description

Telegram supports sharing users and chats through convenient UI since [February, 2023](https://core.telegram.org/bots/api#february-3-2023) and while aiogram has support for `user_shared` and `chat_shared` params, it doesn't detect them in `message.content_type` helper. This PR fixes this error.

## Type of change

- [x] Bug fix (non-breaking change which fixes an issue)

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)
codecov[bot]: ## [Codecov](https://app.codecov.io/gh/aiogram/aiogram/pull/1252?src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=aiogram) Report
> Merging [#1252](https://app.codecov.io/gh/aiogram/aiogram/pull/1252?src=pr&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=aiogram) (da082a0) into [dev-3.x](https://app.codecov.io/gh/aiogram/aiogram/commit/4e7c66726f2cae51c76a34388846444caebcaa1a?el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=aiogram) (4e7c667) will **decrease** coverage by `0.03%`.
> Report is 5 commits behind head on dev-3.x.
> The diff coverage is `50.00%`.

[![Impacted file tree graph](https://app.codecov.io/gh/aiogram/aiogram/pull/1252/graphs/tree.svg?width=650&height=150&src=pr&token=cFqEr3p9MX&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=aiogram)](https://app.codecov.io/gh/aiogram/aiogram/pull/1252?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=aiogram)

```diff
@@             Coverage Diff             @@
##           dev-3.x    #1252      +/-   ##
===========================================
- Coverage   100.00%   99.97%   -0.03%     
===========================================
  Files          364      364              
  Lines         9309     9313       +4     
===========================================
+ Hits          9309     9311       +2     
- Misses           0        2       +2     
```

| Flag | Coverage Δ | |
|---|---|---|
| unittests | `99.97% <50.00%> (-0.03%)` | :arrow_down: |

Flags with carried forward coverage won't be shown. [Click here](https://docs.codecov.io/docs/carryforward-flags?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=aiogram#carryforward-flags-in-the-pull-request-comment) to find out more.

| [Files Changed](https://app.codecov.io/gh/aiogram/aiogram/pull/1252?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=aiogram) | Coverage Δ | |
|---|---|---|
| [aiogram/types/message.py](https://app.codecov.io/gh/aiogram/aiogram/pull/1252?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=aiogram#diff-YWlvZ3JhbS90eXBlcy9tZXNzYWdlLnB5) | `99.57% <50.00%> (-0.43%)` | :arrow_down: |


Patch:
diff --git a/CHANGES/1252.bugfix.rst b/CHANGES/1252.bugfix.rst
new file mode 100644
index 00000000..640f9228
--- /dev/null
+++ b/CHANGES/1252.bugfix.rst
@@ -0,0 +1,1 @@
+Fixed missing message content types (:code:`ContentType.USER_SHARED`, :code:`ContentType.CHAT_SHARED`)
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/types/message.py b/aiogram/types/message.py
index 610f162c..c8fd7c5a 100644
--- a/aiogram/types/message.py
+++ b/aiogram/types/message.py
@@ -492,6 +492,10 @@ class Message(TelegramObject):
             return ContentType.VIDEO_CHAT_PARTICIPANTS_INVITED
         if self.web_app_data:
             return ContentType.WEB_APP_DATA
+        if self.user_shared:
+            return ContentType.USER_SHARED
+        if self.chat_shared:
+            return ContentType.CHAT_SHARED
 
         return ContentType.UNKNOWN
 
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_api/test_types/test_message.py b/tests/test_api/test_types/test_message.py
index 1b15327f..f0a34b09 100644
--- a/tests/test_api/test_types/test_message.py
+++ b/tests/test_api/test_types/test_message.py
@@ -38,6 +38,7 @@ from aiogram.types import (
     Animation,
     Audio,
     Chat,
+    ChatShared,
     Contact,
     Dice,
     Document,
@@ -62,6 +63,7 @@ from aiogram.types import (
     Sticker,
     SuccessfulPayment,
     User,
+    UserShared,
     Venue,
     Video,
     VideoChatEnded,
@@ -439,6 +441,20 @@ TEST_FORUM_TOPIC_REOPENED = Message(
     from_user=User(id=42, is_bot=False, first_name="Test"),
     forum_topic_reopened=ForumTopicReopened(),
 )
+TEST_USER_SHARED = Message(
+    message_id=42,
+    date=datetime.datetime.now(),
+    chat=Chat(id=42, type="private"),
+    from_user=User(id=42, is_bot=False, first_name="Test"),
+    user_shared=UserShared(request_id=42, user_id=42),
+)
+TEST_CHAT_SHARED = Message(
+    message_id=42,
+    date=datetime.datetime.now(),
+    chat=Chat(id=42, type="private"),
+    from_user=User(id=42, is_bot=False, first_name="Test"),
+    chat_shared=ChatShared(request_id=42, chat_id=42),
+)
 TEST_MESSAGE_UNKNOWN = Message(
     message_id=42,
     date=datetime.datetime.now(),
@@ -498,6 +514,8 @@ class TestMessage:
             [TEST_FORUM_TOPIC_EDITED, ContentType.FORUM_TOPIC_EDITED],
             [TEST_FORUM_TOPIC_CLOSED, ContentType.FORUM_TOPIC_CLOSED],
             [TEST_FORUM_TOPIC_REOPENED, ContentType.FORUM_TOPIC_REOPENED],
+            [TEST_USER_SHARED, ContentType.USER_SHARED],
+            [TEST_CHAT_SHARED, ContentType.CHAT_SHARED],
             [TEST_MESSAGE_UNKNOWN, ContentType.UNKNOWN],
         ],
     )
@@ -642,6 +660,8 @@ class TestMessage:
             [TEST_MESSAGE_VIDEO_CHAT_ENDED, None],
             [TEST_MESSAGE_VIDEO_CHAT_PARTICIPANTS_INVITED, None],
             [TEST_MESSAGE_DICE, SendDice],
+            [TEST_USER_SHARED, None],
+            [TEST_CHAT_SHARED, None],
             [TEST_MESSAGE_UNKNOWN, None],
         ],
     )
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(