Repository: aiogram/aiogram
Pull request #1144 (2023-03-13 01:15:52)

storage cleanup
# Description

storage cleanup.


This change shouldn't affect you if you're writing bots, but if you're implementing a storage provider it might break your code.

## Type of change

Please delete options that are not relevant.

- [x] Internal improvements
- [x] New feature (non-breaking change which adds functionality)


# 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)
codecov[bot]: ## [Codecov](https://codecov.io/gh/aiogram/aiogram/pull/1144?src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=aiogram) Report
> Merging [#1144](https://codecov.io/gh/aiogram/aiogram/pull/1144?src=pr&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=aiogram) (b177883) into [dev-3.x](https://codecov.io/gh/aiogram/aiogram/commit/fea1b7b0a385e4dd701ea644fd6e9052ec95c587?el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=aiogram) (fea1b7b) will **not change** coverage.
> The diff coverage is `100.00%`.

[![Impacted file tree graph](https://codecov.io/gh/aiogram/aiogram/pull/1144/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://codecov.io/gh/aiogram/aiogram/pull/1144?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     #1144   +/-   ##
=========================================
  Coverage   100.00%   100.00%           
=========================================
  Files          354       354           
  Lines         8791      8791           
=========================================
  Hits          8791      8791           
```

| Flag | Coverage Δ | |
|---|---|---|
| unittests | `100.00% <100.00%> (ø)` | |

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.

| [Impacted Files](https://codecov.io/gh/aiogram/aiogram/pull/1144?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=aiogram) | Coverage Δ | |
|---|---|---|
| [aiogram/fsm/context.py](https://codecov.io/gh/aiogram/aiogram/pull/1144?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=aiogram#diff-YWlvZ3JhbS9mc20vY29udGV4dC5weQ==) | `100.00% <100.00%> (ø)` | |
| [aiogram/fsm/middleware.py](https://codecov.io/gh/aiogram/aiogram/pull/1144?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=aiogram#diff-YWlvZ3JhbS9mc20vbWlkZGxld2FyZS5weQ==) | `100.00% <100.00%> (ø)` | |
| [aiogram/fsm/storage/base.py](https://codecov.io/gh/aiogram/aiogram/pull/1144?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=aiogram#diff-YWlvZ3JhbS9mc20vc3RvcmFnZS9iYXNlLnB5) | `100.00% <100.00%> (ø)` | |
| [aiogram/fsm/storage/memory.py](https://codecov.io/gh/aiogram/aiogram/pull/1144?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=aiogram#diff-YWlvZ3JhbS9mc20vc3RvcmFnZS9tZW1vcnkucHk=) | `100.00% <100.00%> (ø)` | |
| [aiogram/fsm/storage/redis.py](https://codecov.io/gh/aiogram/aiogram/pull/1144?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=aiogram#diff-YWlvZ3JhbS9mc20vc3RvcmFnZS9yZWRpcy5weQ==) | `100.00% <100.00%> (ø)` | |


Patch:
diff --git a/.butcher/enums/InlineQueryResultType.yml b/.butcher/enums/InlineQueryResultType.yml
index 74428a3c..2d823438 100644
--- a/.butcher/enums/InlineQueryResultType.yml
+++ b/.butcher/enums/InlineQueryResultType.yml
@@ -5,7 +5,7 @@ description: |
   Source: https://core.telegram.org/bots/api#inlinequeryresult
 multi_parse:
   attribute: type
-  regexp: "must be ([a-z_]+)"
+  regexp: "must be ([a-z_0-9]+)"
   entities:
   - InlineQueryResultCachedAudio
   - InlineQueryResultCachedDocument
diff --git a/CHANGES/1144.misc.rst b/CHANGES/1144.misc.rst
new file mode 100644
index 00000000..8b3109f0
--- /dev/null
+++ b/CHANGES/1144.misc.rst
@@ -0,0 +1,1 @@
+Removed bot parameters from storages
diff --git a/CHANGES/1146.bugfix.rst b/CHANGES/1146.bugfix.rst
new file mode 100644
index 00000000..fbe88e4c
--- /dev/null
+++ b/CHANGES/1146.bugfix.rst
@@ -0,0 +1,4 @@
+Change type of result in InlineQueryResult enum for `InlineQueryResultCachedMpeg4Gif`
+and `InlineQueryResultMpeg4Gif` to more correct according to documentation.
+
+Change regexp for entities parsing to more correct (`InlineQueryResultType.yml`).
diff --git a/CHANGES/1147.feature b/CHANGES/1147.feature
new file mode 100644
index 00000000..fa34e703
--- /dev/null
+++ b/CHANGES/1147.feature
@@ -0,0 +1,1 @@
+If router does not support custom event it does not break and passes it to included routers
\ No newline at end of file
diff --git a/CHANGES/1155.bugfix.rst b/CHANGES/1155.bugfix.rst
new file mode 100644
index 00000000..753ffc96
--- /dev/null
+++ b/CHANGES/1155.bugfix.rst
@@ -0,0 +1,1 @@
+Fixed signature of startup/shutdown events to include the **dispatcher.workflow_data as the handler arguments.
diff --git a/aiogram/dispatcher/dispatcher.py b/aiogram/dispatcher/dispatcher.py
index 71791f72..8485d83c 100644
--- a/aiogram/dispatcher/dispatcher.py
+++ b/aiogram/dispatcher/dispatcher.py
@@ -80,9 +80,9 @@ class Dispatcher(Router):
         # FSM middleware should always be registered after User context middleware
         # because here is used context from previous step
         self.fsm = FSMContextMiddleware(
-            storage=storage if storage else MemoryStorage(),
+            storage=storage or MemoryStorage(),
             strategy=fsm_strategy,
-            events_isolation=events_isolation if events_isolation else DisabledEventIsolation(),
+            events_isolation=events_isolation or DisabledEventIsolation(),
         )
         if not disable_fsm:
             # Note that when FSM middleware is disabled, the event isolation is also disabled
@@ -251,14 +251,14 @@ class Dispatcher(Router):
         try:
             update_type = update.event_type
             event = update.event
-        except UpdateTypeLookupError:
+        except UpdateTypeLookupError as e:
             warnings.warn(
                 "Detected unknown update type.\n"
                 "Seems like Telegram Bot API was updated and you have "
                 "installed not latest version of aiogram framework",
                 RuntimeWarning,
             )
-            raise SkipHandler()
+            raise SkipHandler() from e
 
         kwargs.update(event_update=update)
 
@@ -373,7 +373,7 @@ class Dispatcher(Router):
         loop = asyncio.get_running_loop()
         waiter = loop.create_future()
 
-        def release_waiter(*args: Any) -> None:
+        def release_waiter(*_: Any) -> None:
             if not waiter.done():
                 waiter.set_result(None)
 
@@ -488,8 +488,13 @@ class Dispatcher(Router):
                         signal.SIGINT, self._signal_stop_polling, signal.SIGINT
                     )
 
-            workflow_data = {"dispatcher": self, "bots": bots, "bot": bots[-1]}
-            workflow_data.update(kwargs)
+            workflow_data = {
+                "dispatcher": self,
+                "bots": bots,
+                "bot": bots[-1],
+                **kwargs,
+                **self.workflow_data,
+            }
             await self.emit_startup(**workflow_data)
             loggers.dispatcher.info("Start polling")
             try:
diff --git a/aiogram/dispatcher/event/telegram.py b/aiogram/dispatcher/event/telegram.py
index bb943004..a468816c 100644
--- a/aiogram/dispatcher/event/telegram.py
+++ b/aiogram/dispatcher/event/telegram.py
@@ -49,8 +49,9 @@ class TelegramEventObserver:
     def _resolve_middlewares(self) -> List[MiddlewareType[TelegramObject]]:
         middlewares: List[MiddlewareType[TelegramObject]] = []
         for router in reversed(tuple(self.router.chain_head)):
-            observer = router.observers[self.event_name]
-            middlewares.extend(observer.middleware)
+            observer = router.observers.get(self.event_name)
+            if observer:
+                middlewares.extend(observer.middleware)
 
         return middlewares
 
diff --git a/aiogram/dispatcher/router.py b/aiogram/dispatcher/router.py
index 86359ce0..85c41ae8 100644
--- a/aiogram/dispatcher/router.py
+++ b/aiogram/dispatcher/router.py
@@ -105,27 +105,31 @@ class Router:
 
     async def propagate_event(self, update_type: str, event: TelegramObject, **kwargs: Any) -> Any:
         kwargs.update(event_router=self)
-        observer = self.observers[update_type]
+        observer = self.observers.get(update_type)
 
         async def _wrapped(telegram_event: TelegramObject, **data: Any) -> Any:
             return await self._propagate_event(
                 observer=observer, update_type=update_type, event=telegram_event, **data
             )
 
-        return await observer.wrap_outer_middleware(_wrapped, event=event, data=kwargs)
+        if observer:
+            return await observer.wrap_outer_middleware(_wrapped, event=event, data=kwargs)
+        return await _wrapped(event, **kwargs)
 
     async def _propagate_event(
         self,
-        observer: TelegramEventObserver,
+        observer: Optional[TelegramEventObserver],
         update_type: str,
         event: TelegramObject,
         **kwargs: Any,
     ) -> Any:
-        response = await observer.trigger(event, **kwargs)
-        if response is REJECTED:
-            return UNHANDLED
-        if response is not UNHANDLED:
-            return response
+        response = UNHANDLED
+        if observer:
+            response = await observer.trigger(event, **kwargs)
+            if response is REJECTED:
+                return UNHANDLED
+            if response is not UNHANDLED:
+                return response
 
         for router in self.sub_routers:
             response = await router.propagate_event(update_type=update_type, event=event, **kwargs)
diff --git a/aiogram/enums/inline_query_result_type.py b/aiogram/enums/inline_query_result_type.py
index 771dc498..ae20045a 100644
--- a/aiogram/enums/inline_query_result_type.py
+++ b/aiogram/enums/inline_query_result_type.py
@@ -11,7 +11,7 @@ class InlineQueryResultType(str, Enum):
     AUDIO = "audio"
     DOCUMENT = "document"
     GIF = "gif"
-    MPEG = "mpeg"
+    MPEG4_GIF = "mpeg4_gif"
     PHOTO = "photo"
     STICKER = "sticker"
     VIDEO = "video"
diff --git a/aiogram/filters/magic_data.py b/aiogram/filters/magic_data.py
index 351c8e4f..5b3c31d7 100644
--- a/aiogram/filters/magic_data.py
+++ b/aiogram/filters/magic_data.py
@@ -18,7 +18,7 @@ class MagicData(Filter):
 
     async def __call__(self, event: TelegramObject, *args: Any, **kwargs: Any) -> Any:
         return self.magic_data.resolve(
-            AttrDict({"event": event, **{k: v for k, v in enumerate(args)}, **kwargs})
+            AttrDict({"event": event, **dict(enumerate(args)), **kwargs})
         )
 
     def __str__(self) -> str:
diff --git a/aiogram/fsm/context.py b/aiogram/fsm/context.py
index a66bcdbd..53a8ea46 100644
--- a/aiogram/fsm/context.py
+++ b/aiogram/fsm/context.py
@@ -1,33 +1,31 @@
 from typing import Any, Dict, Optional
 
-from aiogram import Bot
 from aiogram.fsm.storage.base import BaseStorage, StateType, StorageKey
 
 
 class FSMContext:
-    def __init__(self, bot: Bot, storage: BaseStorage, key: StorageKey) -> None:
-        self.bot = bot
+    def __init__(self, storage: BaseStorage, key: StorageKey) -> None:
         self.storage = storage
         self.key = key
 
     async def set_state(self, state: StateType = None) -> None:
-        await self.storage.set_state(bot=self.bot, key=self.key, state=state)
+        await self.storage.set_state(key=self.key, state=state)
 
     async def get_state(self) -> Optional[str]:
-        return await self.storage.get_state(bot=self.bot, key=self.key)
+        return await self.storage.get_state(key=self.key)
 
     async def set_data(self, data: Dict[str, Any]) -> None:
-        await self.storage.set_data(bot=self.bot, key=self.key, data=data)
+        await self.storage.set_data(key=self.key, data=data)
 
     async def get_data(self) -> Dict[str, Any]:
-        return await self.storage.get_data(bot=self.bot, key=self.key)
+        return await self.storage.get_data(key=self.key)
 
     async def update_data(
         self, data: Optional[Dict[str, Any]] = None, **kwargs: Any
     ) -> Dict[str, Any]:
         if data:
             kwargs.update(data)
-        return await self.storage.update_data(bot=self.bot, key=self.key, data=kwargs)
+        return await self.storage.update_data(key=self.key, data=kwargs)
 
     async def clear(self) -> None:
         await self.set_state(state=None)
diff --git a/aiogram/fsm/middleware.py b/aiogram/fsm/middleware.py
index 11faca61..0232ff0a 100644
--- a/aiogram/fsm/middleware.py
+++ b/aiogram/fsm/middleware.py
@@ -35,7 +35,7 @@ class FSMContextMiddleware(BaseMiddleware):
         data["fsm_storage"] = self.storage
         if context:
             data.update({"state": context, "raw_state": await context.get_state()})
-            async with self.events_isolation.lock(bot=bot, key=context.key):
+            async with self.events_isolation.lock(key=context.key):
                 return await handler(event, data)
         return await handler(event, data)
 
@@ -76,7 +76,6 @@ class FSMContextMiddleware(BaseMiddleware):
         destiny: str = DEFAULT_DESTINY,
     ) -> FSMContext:
         return FSMContext(
-            bot=bot,
             storage=self.storage,
             key=StorageKey(
                 user_id=user_id,
diff --git a/aiogram/fsm/storage/base.py b/aiogram/fsm/storage/base.py
index 70c62f34..b3551060 100644
--- a/aiogram/fsm/storage/base.py
+++ b/aiogram/fsm/storage/base.py
@@ -3,7 +3,6 @@ from contextlib import asynccontextmanager
 from dataclasses import dataclass
 from typing import Any, AsyncGenerator, Dict, Optional, Union
 
-from aiogram import Bot
 from aiogram.fsm.state import State
 
 StateType = Optional[Union[str, State]]
@@ -25,61 +24,56 @@ class BaseStorage(ABC):
     """
 
     @abstractmethod
-    async def set_state(self, bot: Bot, key: StorageKey, state: StateType = None) -> None:
+    async def set_state(self, key: StorageKey, state: StateType = None) -> None:
         """
         Set state for specified key
 
-        :param bot: instance of the current bot
         :param key: storage key
         :param state: new state
         """
         pass
 
     @abstractmethod
-    async def get_state(self, bot: Bot, key: StorageKey) -> Optional[str]:
+    async def get_state(self, key: StorageKey) -> Optional[str]:
         """
         Get key state
 
-        :param bot: instance of the current bot
         :param key: storage key
         :return: current state
         """
         pass
 
     @abstractmethod
-    async def set_data(self, bot: Bot, key: StorageKey, data: Dict[str, Any]) -> None:
+    async def set_data(self, key: StorageKey, data: Dict[str, Any]) -> None:
         """
         Write data (replace)
 
-        :param bot: instance of the current bot
         :param key: storage key
         :param data: new data
         """
         pass
 
     @abstractmethod
-    async def get_data(self, bot: Bot, key: StorageKey) -> Dict[str, Any]:
+    async def get_data(self, key: StorageKey) -> Dict[str, Any]:
         """
         Get current data for key
 
-        :param bot: instance of the current bot
         :param key: storage key
         :return: current data
         """
         pass
 
-    async def update_data(self, bot: Bot, key: StorageKey, data: Dict[str, Any]) -> Dict[str, Any]:
+    async def update_data(self, key: StorageKey, data: Dict[str, Any]) -> Dict[str, Any]:
         """
         Update date in the storage for key (like dict.update)
 
-        :param bot: instance of the current bot
         :param key: storage key
         :param data: partial data
         :return: new data
         """
-        current_data = await self.get_data(bot=bot, key=key)
+        current_data = await self.get_data(key=key)
         current_data.update(data)
-        await self.set_data(bot=bot, key=key, data=current_data)
+        await self.set_data(key=key, data=current_data)
         return current_data.copy()
 
     @abstractmethod
@@ -93,12 +87,11 @@ class BaseStorage(ABC):
 class BaseEventIsolation(ABC):
     @abstractmethod
     @asynccontextmanager
-    async def lock(self, bot: Bot, key: StorageKey) -> AsyncGenerator[None, None]:
+    async def lock(self, key: StorageKey) -> AsyncGenerator[None, None]:
         """
         Isolate events with lock.
         Will be used as context manager
 
-        :param bot: instance of the current bot
         :param key: storage key
         :return: An async generator
         """
diff --git a/aiogram/fsm/storage/memory.py b/aiogram/fsm/storage/memory.py
index 8be371b6..d80a9fff 100644
--- a/aiogram/fsm/storage/memory.py
+++ b/aiogram/fsm/storage/memory.py
@@ -4,7 +4,6 @@ from contextlib import asynccontextmanager
 from dataclasses import dataclass, field
 from typing import Any, AsyncGenerator, DefaultDict, Dict, Hashable, Optional
 
-from aiogram import Bot
 from aiogram.fsm.state import State
 from aiogram.fsm.storage.base import (
     BaseEventIsolation,
@@ -38,22 +37,22 @@ class MemoryStorage(BaseStorage):
     async def close(self) -> None:
         pass
 
-    async def set_state(self, bot: Bot, key: StorageKey, state: StateType = None) -> None:
+    async def set_state(self, key: StorageKey, state: StateType = None) -> None:
         self.storage[key].state = state.state if isinstance(state, State) else state
 
-    async def get_state(self, bot: Bot, key: StorageKey) -> Optional[str]:
+    async def get_state(self, key: StorageKey) -> Optional[str]:
         return self.storage[key].state
 
-    async def set_data(self, bot: Bot, key: StorageKey, data: Dict[str, Any]) -> None:
+    async def set_data(self, key: StorageKey, data: Dict[str, Any]) -> None:
         self.storage[key].data = data.copy()
 
-    async def get_data(self, bot: Bot, key: StorageKey) -> Dict[str, Any]:
+    async def get_data(self, key: StorageKey) -> Dict[str, Any]:
         return self.storage[key].data.copy()
 
 
 class DisabledEventIsolation(BaseEventIsolation):
     @asynccontextmanager
-    async def lock(self, bot: Bot, key: StorageKey) -> AsyncGenerator[None, None]:
+    async def lock(self, key: StorageKey) -> AsyncGenerator[None, None]:
         yield
 
     async def close(self) -> None:
@@ -66,7 +65,7 @@ class SimpleEventIsolation(BaseEventIsolation):
         self._locks: DefaultDict[Hashable, Lock] = defaultdict(Lock)
 
     @asynccontextmanager
-    async def lock(self, bot: Bot, key: StorageKey) -> AsyncGenerator[None, None]:
+    async def lock(self, key: StorageKey) -> AsyncGenerator[None, None]:
         lock = self._locks[key]
         async with lock:
             yield
diff --git a/aiogram/fsm/storage/redis.py b/aiogram/fsm/storage/redis.py
index 2d8d5076..76450f86 100644
--- a/aiogram/fsm/storage/redis.py
+++ b/aiogram/fsm/storage/redis.py
@@ -1,13 +1,13 @@
+import json
 from abc import ABC, abstractmethod
 from contextlib import asynccontextmanager
-from typing import Any, AsyncGenerator, Dict, Literal, Optional, cast
+from typing import Any, AsyncGenerator, Callable, Dict, Literal, Optional, cast
 
 from redis.asyncio.client import Redis
 from redis.asyncio.connection import ConnectionPool
 from redis.asyncio.lock import Lock
 from redis.typing import ExpiryT
 
-from aiogram import Bot
 from aiogram.fsm.state import State
 from aiogram.fsm.storage.base import (
     DEFAULT_DESTINY,
@@ -18,6 +18,8 @@ from aiogram.fsm.storage.base import (
 )
 
 DEFAULT_REDIS_LOCK_KWARGS = {"timeout": 60}
+_JsonLoads = Callable[..., Any]
+_JsonDumps = Callable[..., str]
 
 
 class KeyBuilder(ABC):
@@ -93,13 +95,14 @@ class RedisStorage(BaseStorage):
         key_builder: Optional[KeyBuilder] = None,
         state_ttl: Optional[ExpiryT] = None,
         data_ttl: Optional[ExpiryT] = None,
+        json_loads: _JsonLoads = json.loads,
+        json_dumps: _JsonDumps = json.dumps,
     ) -> None:
         """
         :param redis: Instance of Redis connection
         :param key_builder: builder that helps to convert contextual key to string
         :param state_ttl: TTL for state records
         :param data_ttl: TTL for data records
-        :param lock_kwargs: Custom arguments for Redis lock
         """
         if key_builder is None:
             key_builder = DefaultKeyBuilder()
@@ -107,6 +110,8 @@ class RedisStorage(BaseStorage):
         self.key_builder = key_builder
         self.state_ttl = state_ttl
         self.data_ttl = data_ttl
+        self.json_loads = json_loads
+        self.json_dumps = json_dumps
 
     @classmethod
     def from_url(
@@ -134,7 +139,6 @@ class RedisStorage(BaseStorage):
 
     async def set_state(
         self,
-        bot: Bot,
         key: StorageKey,
         state: StateType = None,
     ) -> None:
@@ -150,7 +154,6 @@ class RedisStorage(BaseStorage):
 
     async def get_state(
         self,
-        bot: Bot,
         key: StorageKey,
     ) -> Optional[str]:
         redis_key = self.key_builder.build(key, "state")
@@ -161,7 +164,6 @@ class RedisStorage(BaseStorage):
 
     async def set_data(
         self,
-        bot: Bot,
         key: StorageKey,
         data: Dict[str, Any],
     ) -> None:
@@ -171,13 +173,12 @@ class RedisStorage(BaseStorage):
             return
         await self.redis.set(
             redis_key,
-            bot.session.json_dumps(data),
+            self.json_dumps(data),
             ex=self.data_ttl,
         )
 
     async def get_data(
         self,
-        bot: Bot,
         key: StorageKey,
     ) -> Dict[str, Any]:
         redis_key = self.key_builder.build(key, "data")
@@ -186,7 +187,7 @@ class RedisStorage(BaseStorage):
             return {}
         if isinstance(value, bytes):
             value = value.decode("utf-8")
-        return cast(Dict[str, Any], bot.session.json_loads(value))
+        return cast(Dict[str, Any], self.json_loads(value))
 
 
 class RedisEventIsolation(BaseEventIsolation):
@@ -220,7 +221,6 @@ class RedisEventIsolation(BaseEventIsolation):
     @asynccontextmanager
     async def lock(
         self,
-        bot: Bot,
         key: StorageKey,
     ) -> AsyncGenerator[None, None]:
         redis_key = self.key_builder.build(key, "lock")
diff --git a/aiogram/types/inline_query_result_cached_mpeg4_gif.py b/aiogram/types/inline_query_result_cached_mpeg4_gif.py
index ab7a2a9b..539de025 100644
--- a/aiogram/types/inline_query_result_cached_mpeg4_gif.py
+++ b/aiogram/types/inline_query_result_cached_mpeg4_gif.py
@@ -21,7 +21,7 @@ class InlineQueryResultCachedMpeg4Gif(InlineQueryResult):
     Source: https://core.telegram.org/bots/api#inlinequeryresultcachedmpeg4gif
     """
 
-    type: str = Field(InlineQueryResultType.MPEG, const=True)
+    type: str = Field(InlineQueryResultType.MPEG4_GIF, const=True)
     """Type of the result, must be *mpeg4_gif*"""
     id: str
     """Unique identifier for this result, 1-64 bytes"""
diff --git a/aiogram/types/inline_query_result_mpeg4_gif.py b/aiogram/types/inline_query_result_mpeg4_gif.py
index 44a61fee..06c73620 100644
--- a/aiogram/types/inline_query_result_mpeg4_gif.py
+++ b/aiogram/types/inline_query_result_mpeg4_gif.py
@@ -21,7 +21,7 @@ class InlineQueryResultMpeg4Gif(InlineQueryResult):
     Source: https://core.telegram.org/bots/api#inlinequeryresultmpeg4gif
     """
 
-    type: str = Field(InlineQueryResultType.MPEG, const=True)
+    type: str = Field(InlineQueryResultType.MPEG4_GIF, const=True)
     """Type of the result, must be *mpeg4_gif*"""
     id: str
     """Unique identifier for this result, 1-64 bytes"""
diff --git a/aiogram/utils/deep_linking.py b/aiogram/utils/deep_linking.py
index ec3f9bff..3d27633f 100644
--- a/aiogram/utils/deep_linking.py
+++ b/aiogram/utils/deep_linking.py
@@ -15,8 +15,9 @@ Basic link example:
 
     .. code-block:: python
 
-        from aiogram.utils.deep_linking import get_start_link
-        link = await get_start_link('foo')
+        from aiogram.utils.deep_linking import create_start_link
+        
+        link = await create_start_link(bot, 'foo')
 
         # result: 'https://t.me/MyBot?start=foo'
 
@@ -24,20 +25,21 @@ Encoded link example:
 
     .. code-block:: python
 
-        from aiogram.utils.deep_linking import get_start_link
+        from aiogram.utils.deep_linking import create_start_link
 
-        link = await get_start_link('foo', encode=True)
+        link = await create_start_link(bot, 'foo', encode=True)
         # result: 'https://t.me/MyBot?start=Zm9v'
 
 Decode it back example:
     .. code-block:: python
 
         from aiogram.utils.deep_linking import decode_payload
+        from aiogram.filters import CommandStart, CommandObject
         from aiogram.types import Message
 
-        @dp.message_handler(commands=["start"])
-        async def handler(message: Message):
-            args = message.get_args()
+        @router.message(CommandStart(deep_link=True))
+        async def handler(message: Message, command: CommandObject):
+            args = command.args
             payload = decode_payload(args)
             await message.answer(f"Your payload: {payload}")
 
diff --git a/docs/api/upload_file.rst b/docs/api/upload_file.rst
index 9399f1dd..715474f7 100644
--- a/docs/api/upload_file.rst
+++ b/docs/api/upload_file.rst
@@ -10,7 +10,7 @@ there are three ways to send files (photos, stickers, audio, media, etc.):
 If the file is already stored somewhere on the Telegram servers or file is available by the URL,
 you don't need to reupload it.
 
-But if you need to upload a new file just use subclasses of `InputFile <types/input_file.md>`__.
+But if you need to upload a new file just use subclasses of `InputFile <types/input_file.html>`__.
 
 Here are the three different available builtin types of input file:
 


Test patch:
diff --git a/tests/test_dispatcher/test_router.py b/tests/test_dispatcher/test_router.py
index 3404822d..1ac78480 100644
--- a/tests/test_dispatcher/test_router.py
+++ b/tests/test_dispatcher/test_router.py
@@ -1,6 +1,7 @@
 import pytest
 
 from aiogram.dispatcher.event.bases import UNHANDLED, SkipHandler, skip
+from aiogram.dispatcher.event.telegram import TelegramEventObserver
 from aiogram.dispatcher.router import Router
 
 
@@ -161,3 +162,20 @@ class TestRouter:
         assert tuple(r2_1.chain_head) == (r2_1, r1)
         assert tuple(r2_2.chain_head) == (r2_2, r1)
         assert tuple(r3.chain_head) == (r3, r2_1, r1)
+
+    async def test_custom_evenv_nested_router(self):
+        r1 = Router()
+        r2 = Router()
+        r3 = Router()
+        r3.observers["custom-event"] = TelegramEventObserver(r3, event_name="custom-event")
+
+        async def handler(evt):
+            return evt
+
+        r1.include_router(r2)
+        r1.include_router(r3)
+        r3.observers["custom-event"].register(handler)
+
+        assert await r1.propagate_event(update_type="custom-event", event=None) is None
+        assert await r2.propagate_event(update_type="custom-event", event=None) is UNHANDLED
+        assert await r3.propagate_event(update_type="custom-event", event=None) is None
diff --git a/tests/test_fsm/storage/test_isolation.py b/tests/test_fsm/storage/test_isolation.py
index 042cc305..8212c955 100644
--- a/tests/test_fsm/storage/test_isolation.py
+++ b/tests/test_fsm/storage/test_isolation.py
@@ -5,7 +5,7 @@ from tests.mocked_bot import MockedBot
 
 
 @pytest.fixture(name="storage_key")
-def create_storate_key(bot: MockedBot):
+def create_storage_key(bot: MockedBot):
     return StorageKey(chat_id=-42, user_id=42, bot_id=bot.id)
 
 
@@ -20,9 +20,8 @@ def create_storate_key(bot: MockedBot):
 class TestIsolations:
     async def test_lock(
         self,
-        bot: MockedBot,
         isolation: BaseEventIsolation,
         storage_key: StorageKey,
     ):
-        async with isolation.lock(bot=bot, key=storage_key):
+        async with isolation.lock(key=storage_key):
             assert True, "You are kidding me?"
diff --git a/tests/test_fsm/storage/test_storages.py b/tests/test_fsm/storage/test_storages.py
index 83871722..3558e33d 100644
--- a/tests/test_fsm/storage/test_storages.py
+++ b/tests/test_fsm/storage/test_storages.py
@@ -5,7 +5,7 @@ from tests.mocked_bot import MockedBot
 
 
 @pytest.fixture(name="storage_key")
-def create_storate_key(bot: MockedBot):
+def create_storage_key(bot: MockedBot):
     return StorageKey(chat_id=-42, user_id=42, bot_id=bot.id)
 
 
@@ -15,33 +15,31 @@ def create_storate_key(bot: MockedBot):
 )
 class TestStorages:
     async def test_set_state(self, bot: MockedBot, storage: BaseStorage, storage_key: StorageKey):
-        assert await storage.get_state(bot=bot, key=storage_key) is None
+        assert await storage.get_state(key=storage_key) is None
 
-        await storage.set_state(bot=bot, key=storage_key, state="state")
-        assert await storage.get_state(bot=bot, key=storage_key) == "state"
-        await storage.set_state(bot=bot, key=storage_key, state=None)
-        assert await storage.get_state(bot=bot, key=storage_key) is None
+        await storage.set_state(key=storage_key, state="state")
+        assert await storage.get_state(key=storage_key) == "state"
+        await storage.set_state(key=storage_key, state=None)
+        assert await storage.get_state(key=storage_key) is None
 
     async def test_set_data(self, bot: MockedBot, storage: BaseStorage, storage_key: StorageKey):
-        assert await storage.get_data(bot=bot, key=storage_key) == {}
+        assert await storage.get_data(key=storage_key) == {}
 
-        await storage.set_data(bot=bot, key=storage_key, data={"foo": "bar"})
-        assert await storage.get_data(bot=bot, key=storage_key) == {"foo": "bar"}
-        await storage.set_data(bot=bot, key=storage_key, data={})
-        assert await storage.get_data(bot=bot, key=storage_key) == {}
+        await storage.set_data(key=storage_key, data={"foo": "bar"})
+        assert await storage.get_data(key=storage_key) == {"foo": "bar"}
+        await storage.set_data(key=storage_key, data={})
+        assert await storage.get_data(key=storage_key) == {}
 
     async def test_update_data(
         self, bot: MockedBot, storage: BaseStorage, storage_key: StorageKey
     ):
-        assert await storage.get_data(bot=bot, key=storage_key) == {}
-        assert await storage.update_data(bot=bot, key=storage_key, data={"foo": "bar"}) == {
-            "foo": "bar"
-        }
-        assert await storage.update_data(bot=bot, key=storage_key, data={"baz": "spam"}) == {
+        assert await storage.get_data(key=storage_key) == {}
+        assert await storage.update_data(key=storage_key, data={"foo": "bar"}) == {"foo": "bar"}
+        assert await storage.update_data(key=storage_key, data={"baz": "spam"}) == {
             "foo": "bar",
             "baz": "spam",
         }
-        assert await storage.get_data(bot=bot, key=storage_key) == {
+        assert await storage.get_data(key=storage_key) == {
             "foo": "bar",
             "baz": "spam",
         }
diff --git a/tests/test_fsm/test_context.py b/tests/test_fsm/test_context.py
index 5c31373c..f0c29911 100644
--- a/tests/test_fsm/test_context.py
+++ b/tests/test_fsm/test_context.py
@@ -13,7 +13,7 @@ def state(bot: MockedBot):
     ctx = storage.storage[key]
     ctx.state = "test"
     ctx.data = {"foo": "bar"}
-    return FSMContext(bot=bot, storage=storage, key=key)
+    return FSMContext(storage=storage, key=key)
 
 
 class TestFSMContext:
@@ -22,15 +22,9 @@ class TestFSMContext:
         ctx = storage.storage[StorageKey(chat_id=-42, user_id=42, bot_id=bot.id)]
         ctx.state = "test"
         ctx.data = {"foo": "bar"}
-        state = FSMContext(
-            bot=bot, storage=storage, key=StorageKey(chat_id=-42, user_id=42, bot_id=bot.id)
-        )
-        state2 = FSMContext(
-            bot=bot, storage=storage, key=StorageKey(chat_id=42, user_id=42, bot_id=bot.id)
-        )
-        state3 = FSMContext(
-            bot=bot, storage=storage, key=StorageKey(chat_id=69, user_id=69, bot_id=bot.id)
-        )
+        state = FSMContext(storage=storage, key=StorageKey(chat_id=-42, user_id=42, bot_id=bot.id))
+        state2 = FSMContext(storage=storage, key=StorageKey(chat_id=42, user_id=42, bot_id=bot.id))
+        state3 = FSMContext(storage=storage, key=StorageKey(chat_id=69, user_id=69, bot_id=bot.id))
 
         assert await state.get_state() == "test"
         assert await state2.get_state() is None
diff --git a/tests/test_utils/test_i18n.py b/tests/test_utils/test_i18n.py
index 434d2e02..71fa2eb7 100644
--- a/tests/test_utils/test_i18n.py
+++ b/tests/test_utils/test_i18n.py
@@ -178,9 +178,7 @@ class TestFSMI18nMiddleware:
     async def test_middleware(self, i18n: I18n, bot: MockedBot, extra):
         middleware = FSMI18nMiddleware(i18n=i18n)
         storage = MemoryStorage()
-        state = FSMContext(
-            bot=bot, storage=storage, key=StorageKey(user_id=42, chat_id=42, bot_id=bot.id)
-        )
+        state = FSMContext(storage=storage, key=StorageKey(user_id=42, chat_id=42, bot_id=bot.id))
         data = {
             "event_from_user": User(id=42, is_bot=False, language_code="it", first_name="Test"),
             "state": state,