From 83847a9ab8f74b80d5e8d096e7bb57a8685aa69c Mon Sep 17 00:00:00 2001 From: Alex Root Junior Date: Fri, 4 Aug 2017 15:57:45 +0300 Subject: [PATCH 1/6] Fix regexp filter (Thanks to @Oleg_Oleg_Oleg). --- aiogram/dispatcher/filters.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/aiogram/dispatcher/filters.py b/aiogram/dispatcher/filters.py index d77a6378..e433b837 100644 --- a/aiogram/dispatcher/filters.py +++ b/aiogram/dispatcher/filters.py @@ -69,11 +69,11 @@ class CommandsFilter(AsyncFilter): class RegexpFilter(Filter): def __init__(self, regexp): - self.regexp = re.compile(regexp) + self.regexp = re.compile(regexp, flags=re.IGNORECASE | re.MULTILINE) def check(self, message): if message.text: - return bool(self.regexp.match(message.text)) + return bool(self.regexp.search(message.text)) class ContentTypeFilter(Filter): From b435e446a55ec427cce82d1afc2eab5c4c0be68d Mon Sep 17 00:00:00 2001 From: Alex Root Junior Date: Fri, 4 Aug 2017 15:59:09 +0300 Subject: [PATCH 2/6] Split FSM storages to different modules. --- aiogram/contrib/fsm_storage/__init__.py | 0 aiogram/contrib/fsm_storage/memory.py | 78 +++++++++++++++++++++++++ aiogram/dispatcher/storage.py | 75 ------------------------ 3 files changed, 78 insertions(+), 75 deletions(-) create mode 100644 aiogram/contrib/fsm_storage/__init__.py create mode 100644 aiogram/contrib/fsm_storage/memory.py diff --git a/aiogram/contrib/fsm_storage/__init__.py b/aiogram/contrib/fsm_storage/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/aiogram/contrib/fsm_storage/memory.py b/aiogram/contrib/fsm_storage/memory.py new file mode 100644 index 00000000..cbd8f243 --- /dev/null +++ b/aiogram/contrib/fsm_storage/memory.py @@ -0,0 +1,78 @@ +import typing + +from aiogram.dispatcher import BaseStorage + + +class MemoryStorage(BaseStorage): + """ + In-memory based states storage. + + This type of storage is not recommended for usage in bots, because you will lost all states after restarting. + """ + + def __init__(self): + self.data = {} + + def _get_chat(self, chat_id): + chat_id = str(chat_id) + if chat_id not in self.data: + self.data[chat_id] = {} + return self.data[chat_id] + + def _get_user(self, chat_id, user_id): + chat = self._get_chat(chat_id) + chat_id = str(chat_id) + user_id = str(user_id) + if user_id not in self.data[chat_id]: + self.data[chat_id][user_id] = {'state': None, 'data': {}} + return self.data[chat_id][user_id] + + async def get_state(self, *, + chat: typing.Union[str, int, None] = None, + user: typing.Union[str, int, None] = None, + default: typing.Optional[str] = None) -> typing.Optional[str]: + chat, user = self.check_address(chat=chat, user=user) + user = self._get_user(chat, user) + return user['state'] + + async def get_data(self, *, + chat: typing.Union[str, int, None] = None, + user: typing.Union[str, int, None] = None, + default: typing.Optional[str] = None) -> typing.Dict: + chat, user = self.check_address(chat=chat, user=user) + user = self._get_user(chat, user) + return user['data'] + + async def update_data(self, *, + chat: typing.Union[str, int, None] = None, + user: typing.Union[str, int, None] = None, + data: typing.Dict = None, **kwargs): + chat, user = self.check_address(chat=chat, user=user) + user = self._get_user(chat, user) + if data is None: + data = [] + user['data'].update(data, **kwargs) + + async def set_state(self, *, + chat: typing.Union[str, int, None] = None, + user: typing.Union[str, int, None] = None, + state: typing.AnyStr = None): + chat, user = self.check_address(chat=chat, user=user) + user = self._get_user(chat, user) + user['state'] = state + + async def set_data(self, *, + chat: typing.Union[str, int, None] = None, + user: typing.Union[str, int, None] = None, + data: typing.Dict = None): + chat, user = self.check_address(chat=chat, user=user) + user = self._get_user(chat, user) + user['data'] = data + + async def reset_state(self, *, + chat: typing.Union[str, int, None] = None, + user: typing.Union[str, int, None] = None, + with_data: typing.Optional[bool] = True): + await self.set_state(chat=chat, user=user, state=None) + if with_data: + await self.set_data(chat=chat, user=user, data={}) diff --git a/aiogram/dispatcher/storage.py b/aiogram/dispatcher/storage.py index e33a0ae5..4deb3b54 100644 --- a/aiogram/dispatcher/storage.py +++ b/aiogram/dispatcher/storage.py @@ -238,78 +238,3 @@ class DisabledStorage(BaseStorage): user: typing.Union[str, int, None] = None, data: typing.Dict = None): pass - - -class MemoryStorage(BaseStorage): - """ - In-memory based states storage. - - This type of storage is not recommended for usage in bots, because you will lost all states after restarting. - """ - - def __init__(self): - self.data = {} - - def _get_chat(self, chat_id): - chat_id = str(chat_id) - if chat_id not in self.data: - self.data[chat_id] = {} - return self.data[chat_id] - - def _get_user(self, chat_id, user_id): - chat = self._get_chat(chat_id) - chat_id = str(chat_id) - user_id = str(user_id) - if user_id not in self.data[chat_id]: - self.data[chat_id][user_id] = {'state': None, 'data': {}} - return self.data[chat_id][user_id] - - async def get_state(self, *, - chat: typing.Union[str, int, None] = None, - user: typing.Union[str, int, None] = None, - default: typing.Optional[str] = None) -> typing.Optional[str]: - chat, user = self.check_address(chat=chat, user=user) - user = self._get_user(chat, user) - return user['state'] - - async def get_data(self, *, - chat: typing.Union[str, int, None] = None, - user: typing.Union[str, int, None] = None, - default: typing.Optional[str] = None) -> typing.Dict: - chat, user = self.check_address(chat=chat, user=user) - user = self._get_user(chat, user) - return user['data'] - - async def update_data(self, *, - chat: typing.Union[str, int, None] = None, - user: typing.Union[str, int, None] = None, - data: typing.Dict = None, **kwargs): - chat, user = self.check_address(chat=chat, user=user) - user = self._get_user(chat, user) - if data is None: - data = [] - user['data'].update(data, **kwargs) - - async def set_state(self, *, - chat: typing.Union[str, int, None] = None, - user: typing.Union[str, int, None] = None, - state: typing.AnyStr = None): - chat, user = self.check_address(chat=chat, user=user) - user = self._get_user(chat, user) - user['state'] = state - - async def set_data(self, *, - chat: typing.Union[str, int, None] = None, - user: typing.Union[str, int, None] = None, - data: typing.Dict = None): - chat, user = self.check_address(chat=chat, user=user) - user = self._get_user(chat, user) - user['data'] = data - - async def reset_state(self, *, - chat: typing.Union[str, int, None] = None, - user: typing.Union[str, int, None] = None, - with_data: typing.Optional[bool] = True): - await self.set_state(chat=chat, user=user, state=None) - if with_data: - await self.set_data(chat=chat, user=user, data={}) From a0a0b26e801e0e02fdc01bf8f9388ea01ec6fe94 Mon Sep 17 00:00:00 2001 From: Alex Root Junior Date: Fri, 4 Aug 2017 16:00:08 +0300 Subject: [PATCH 3/6] Implement RedisStorage (based on aioredis driver) --- aiogram/contrib/fsm_storage/redis.py | 118 +++++++++++++++++++++++++++ 1 file changed, 118 insertions(+) create mode 100644 aiogram/contrib/fsm_storage/redis.py diff --git a/aiogram/contrib/fsm_storage/redis.py b/aiogram/contrib/fsm_storage/redis.py new file mode 100644 index 00000000..f6927958 --- /dev/null +++ b/aiogram/contrib/fsm_storage/redis.py @@ -0,0 +1,118 @@ +""" +This module has redis storage for finite-state machine based on `aioredis `_ driver + +""" + +import typing + +import aioredis + +from aiogram.utils import json +from ...dispatcher.storage import BaseStorage + + +class RedisStorage(BaseStorage): + """ + Simple Redis-base storage for FSM. + + Usage: + + .. codeblock:: python3 + + storage = RedisStorage('localhost', 6379, db=5) + dp = Dispatcher(bot, storage=storage) + + """ + + def __init__(self, host, port, db=None, password=None, ssl=None, loop=None, **kwargs): + self._host = host + self._port = port + self._db: aioredis.RedisConnection = db + self._password = password + self._ssl = ssl + self._loop = loop + self._kwargs = kwargs + + self._redis = None + + @property + async def redis(self) -> aioredis.RedisConnection: + """ + Get Redis connection + + This property is awaitable. + :return: + """ + if self._redis is None: + self._redis = await aioredis.create_connection((self._host, self._port), + db=self._db, password=self._password, ssl=self._ssl, + loop=self._loop, + **self._kwargs) + return self._redis + + async def get_record(self, *, chat: typing.Union[str, int, None] = None, user: typing.Union[str, int, None] = None): + """ + Get record from storage + + :param chat: + :param user: + :return: + """ + chat, user = self.check_address(chat=chat, user=user) + addr = f"{chat}:{user}" + + redis = await self.redis + data = await redis.execute('GET', addr) + if data is None: + return {'state': None, 'data': {}} + return json.loads(data) + + async def set_record(self, *, chat: typing.Union[str, int, None] = None, user: typing.Union[str, int, None] = None, + state=None, data=None): + """ + Write record to storage + + :param chat: + :param user: + :param state: + :param data: + :return: + """ + if data is None: + data = {} + + chat, user = self.check_address(chat=chat, user=user) + addr = f"{chat}:{user}" + + record = {'state': state, 'data': data} + + conn = await self.redis + await conn.execute('SET', addr, json.dumps(record)) + + async def get_state(self, *, chat: typing.Union[str, int, None] = None, user: typing.Union[str, int, None] = None, + default: typing.Optional[str] = None) -> typing.Optional[str]: + record = await self.get_record(chat=chat, user=user) + return record['state'] + + async def get_data(self, *, chat: typing.Union[str, int, None] = None, user: typing.Union[str, int, None] = None, + default: typing.Optional[str] = None) -> typing.Dict: + record = await self.get_record(chat=chat, user=user) + return record['data'] + + async def set_state(self, *, chat: typing.Union[str, int, None] = None, user: typing.Union[str, int, None] = None, + state: typing.Optional[typing.AnyStr] = None): + record = await self.get_record(chat=chat, user=user) + await self.set_record(chat=chat, user=user, state=state, data=record['data']) + + async def set_data(self, *, chat: typing.Union[str, int, None] = None, user: typing.Union[str, int, None] = None, + data: typing.Dict = None): + record = await self.get_record(chat=chat, user=user) + await self.set_record(chat=chat, user=user, state=record['state'], data=data) + + async def update_data(self, *, chat: typing.Union[str, int, None] = None, user: typing.Union[str, int, None] = None, + data: typing.Dict = None, **kwargs): + data = await self.get_data(chat=chat, user=user) + if data is None: + data = [] + data.update(data, **kwargs) + await self.set_data(chat=chat, user=user, data=data) From 44dd71e4617ae1b57406131707306fcea2ac7cfb Mon Sep 17 00:00:00 2001 From: Alex Root Junior Date: Fri, 4 Aug 2017 16:00:26 +0300 Subject: [PATCH 4/6] Lost `__init__.py` --- aiogram/contrib/__init__.py | 0 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 aiogram/contrib/__init__.py diff --git a/aiogram/contrib/__init__.py b/aiogram/contrib/__init__.py new file mode 100644 index 00000000..e69de29b From 3ab4e51dde96f285449c84ebb8c278cafb031ae6 Mon Sep 17 00:00:00 2001 From: Alex Root Junior Date: Fri, 4 Aug 2017 16:00:54 +0300 Subject: [PATCH 5/6] Oops. Bad warning. --- aiogram/dispatcher/handler.py | 1 - 1 file changed, 1 deletion(-) diff --git a/aiogram/dispatcher/handler.py b/aiogram/dispatcher/handler.py index f5f5bee7..be0201c3 100644 --- a/aiogram/dispatcher/handler.py +++ b/aiogram/dispatcher/handler.py @@ -50,7 +50,6 @@ class Handler: break -@deprecated('This handler will be removed soon.') class NextStepHandler: def __init__(self, dispatcher): self.dispatcher = dispatcher From 86f7656b839b4a6708e9f13989eefde07dbca64c Mon Sep 17 00:00:00 2001 From: Alex Root Junior Date: Fri, 4 Aug 2017 16:01:10 +0300 Subject: [PATCH 6/6] Update FSM example. --- examples/finite_state_machine_example.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/examples/finite_state_machine_example.py b/examples/finite_state_machine_example.py index 89829cb7..9ead40e3 100644 --- a/examples/finite_state_machine_example.py +++ b/examples/finite_state_machine_example.py @@ -1,8 +1,8 @@ import asyncio from aiogram import Bot, types +from aiogram.contrib.fsm_storage.memory import MemoryStorage from aiogram.dispatcher import Dispatcher -from aiogram.dispatcher.storage import MemoryStorage from aiogram.types import ParseMode from aiogram.utils.markdown import text, bold @@ -84,7 +84,7 @@ async def process_age(message: types.Message): await state.update_data(age=int(message.text)) # Configure ReplyKeyboardMarkup - markup = types.ReplyKeyboardMarkup(resize_keyboard=True) + markup = types.ReplyKeyboardMarkup(resize_keyboard=True, selective=True) markup.add("Male", "Female") markup.add("Other")