Skip to content
162 changes: 162 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,162 @@
# Byte-compiled / optimized / DLL files
__pycache__/
*.py[cod]
*$py.class

# C extensions
*.so

# Distribution / packaging
.Python
build/
develop-eggs/
dist/
downloads/
eggs/
.eggs/
lib/
lib64/
parts/
sdist/
var/
wheels/
share/python-wheels/
*.egg-info/
.installed.cfg
*.egg
MANIFEST

# PyInstaller
# Usually these files are written by a python script from a templates
# before PyInstaller builds the exe, so as to inject date/other infos into it.
*.manifest
#*.spec

# Installer logs
pip-log.txt
pip-delete-this-directory.txt

# Unit test / coverage reports
htmlcov/
.tox/
.nox/
.coverage
.coverage.*
.cache
nosetests.xml
coverage.xml
*.cover
*.py,cover
.hypothesis/
.pytest_cache/
cover/

# Translations
*.mo
*.pot

# Django stuff:
*.log
local_settings.py
db.sqlite3
db.sqlite3-journal

# Flask stuff:
instance/
.webassets-cache

# Scrapy stuff:
#.scrapy

# Sphinx documentation
docs/_build/

# PyBuilder
.pybuilder/
target/

# Jupyter Notebook
.ipynb_checkpoints

# IPython
profile_default/
ipython_config.py

# pyenv
# For a library or package, you might want to ignore these files since the code is
# intended to run in multiple environments; otherwise, check them in:
# .python-version

# pipenv
# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
# However, in case of collaboration, if having platform-specific dependencies or dependencies
# having no cross-platform support, pipenv may install dependencies that don't work, or not
# install all needed dependencies.
#Pipfile.lock

# poetry
# Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control.
# This is especially recommended for binary packages to ensure reproducibility, and is more
# commonly ignored for libraries.
# https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control
#poetry.lock

# pdm
# Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control.
#pdm.lock
# pdm stores type_query-wide configurations in .pdm.toml, but it is recommended to not include it
# in version control.
# https://pdm.fming.dev/latest/usage/project/#working-with-version-control
.pdm.toml
.pdm-python
.pdm-build/

# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-type_query/pdm
__pypackages__/

# Celery stuff
celerybeat-schedule
celerybeat.pid

# SageMath parsed files
*.sage.py

# Environments
.env
.venv
env/
venv/
ENV/
env.bak/
venv.bak/

# Spyder type_query settings
.spyderproject
.spyproject

# Rope type_query settings
.ropeproject

# mkdocs documentation
/site

# mypy
.mypy_cache/
.dmypy.json
dmypy.json

# Pyre type checker
.pyre/

# pytype static type analyzer
.pytype/

# Cython debug symbols
cython_debug/

# PyCharm
# JetBrains specific templates is maintained in a separate JetBrains.gitignore that can
# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
# and can be added to the global gitignore or merged into this file. For a more nuclear
# option (not recommended) you can uncomment the following to ignore the entire idea folder.
.idea/
8 changes: 5 additions & 3 deletions aiogram_renderer/bot_mode.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
from typing import Any
from aiogram.fsm.context import FSMContext
from .widgets.media.bytes import FileBytes
from .widgets.media import FileBytes
from .window import Alert


Expand All @@ -14,7 +14,8 @@ class BotMode:

def __init__(self, name: str, values: list[str], alert_window: Alert, has_custom_handler: bool = False):
for widget in alert_window._widgets:
assert not isinstance(widget, FileBytes), ValueError("В alert_window не может быть файл с байтами")
if isinstance(widget, FileBytes):
raise ValueError("В alert_window не может быть файл с байтами")

self.name = name
self.values = values
Expand Down Expand Up @@ -116,7 +117,8 @@ async def update_mode(self, mode: str) -> str:

async def get_active_value(self, name: str) -> None:
dict_modes = await self.get_dict_modes()
assert dict_modes[name], ValueError("У бота нет данного режима")
if not dict_modes.get(name):
raise ValueError("У бота нет данного режима")
fsm_modes = await self.get_fsm_modes()
# Активным считается первое значение режима
return fsm_modes[name][0]
Expand Down
13 changes: 13 additions & 0 deletions aiogram_renderer/callback_data.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
from aiogram.filters.callback_data import CallbackData


class DPanelCD(CallbackData, prefix="__dpanel__"):
page: int
panel_name: str

class ModeCD(CallbackData, prefix="__mode__"):
name: str

class ComeToCD(CallbackData, prefix="__cometo__"):
group: str
state: str
2 changes: 1 addition & 1 deletion aiogram_renderer/enums.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,4 +5,4 @@ class RenderMode(str, Enum):
EDIT = "edit"
DELETE_AND_SEND = "delete_and_send"
ANSWER = "answer"
REPLY = "reply"
REPLY = "reply"
76 changes: 38 additions & 38 deletions aiogram_renderer/filters.py
Original file line number Diff line number Diff line change
@@ -1,54 +1,54 @@
from aiogram.filters import BaseFilter
from aiogram.types import Message, CallbackQuery

from .callback_data import ModeCD
from .renderer import Renderer


class IsModeWithNotCustomHandler(BaseFilter):
async def __call__(self, event: Message | CallbackQuery, renderer: Renderer) -> bool:
# Если режимы заданы
if renderer.bot_modes is not None:
mode = None
# Для CallbackQuery проверяем правильно ли задан callback_data по системному префиксу
if isinstance(event, CallbackQuery):
if event.data.startswith("__mode__:"):
# Для колбека берем название мода, указанное после "__mode__:"
mode_name = event.data.replace("__mode__:", "")
# Проверяем нет ли у данного режима своего хендлера
mode = await renderer.bot_modes.get_mode_by_name(name=mode_name)

# Для Message, ищем его среди списков значений модов и выводим по найденному названию мода
else:
modes_values = await renderer.bot_modes.get_modes_values()
if event.text in modes_values:
# Проверяем нет ли у данного режима своего хендлера
mode = await renderer.bot_modes.get_mode_by_value(value=event.text)

# Проверяем нашелся ли режим и есть ли у него пользовательский хендлер
if (mode is not None) and (not mode.has_custom_handler):
return True

return False
bot_modes = renderer.bot_modes
if not bot_modes: # Если режимы не заданы
return False

mode = None
# Для CallbackQuery проверяем правильно ли задан callback_data по системному префиксу
if isinstance(event, CallbackQuery):
try:
callback_data = ModeCD.unpack(event.data)
mode = await bot_modes.get_mode_by_name(name=callback_data.name) if callback_data else None
except (TypeError, ValueError):
pass
elif isinstance(event, Message): # Ищем его среди списков значений модов и выводим по найденному названию мода
modes_values = await bot_modes.get_modes_values()
if event.text in modes_values:
mode = await bot_modes.get_mode_by_value(value=event.text)

return bool(mode and not mode.has_custom_handler)


class IsMode(BaseFilter):
def __init__(self, name: str):
self.name = name

async def __call__(self, event: Message | CallbackQuery, renderer: Renderer) -> bool:
# Проверяем заданы ли режимы и есть ли такой режим
if renderer.bot_modes is not None:
dict_modes = await renderer.bot_modes.get_dict_modes()
if self.name in dict_modes.keys():
mode = await renderer.bot_modes.get_mode_by_name(name=self.name)
# Проверяем равен ли коллбек заданному режиму
if isinstance(event, CallbackQuery):
if (event.data == "__mode__:" + self.name) and (mode is not None):
return True
# Проверяем есть ли значение Reply text в values режима
elif isinstance(event, Message):
if (event.text in mode.values) and (mode is not None):
return True
else:
raise ValueError("Такого режима нет")
bot_modes = renderer.bot_modes
if not bot_modes: # Если режимы не заданы
return False

dict_modes = await bot_modes.get_dict_modes()
if self.name not in dict_modes:
raise ValueError("Такого режима нет")

mode = await bot_modes.get_mode_by_name(name=self.name)
if not mode:
return False

if isinstance(event, CallbackQuery):
# Проверяем равен ли сallback заданному режиму
return event.data == ModeCD(name=self.name).pack()
elif isinstance(event, Message):
# Проверяем есть ли значение Reply text в values режима
return event.text in mode.values

return False
22 changes: 10 additions & 12 deletions aiogram_renderer/handlers/inline_router.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
from aiogram import Router, F
from aiogram.fsm.context import FSMContext
from aiogram.types import CallbackQuery

from aiogram_renderer.callback_data import DPanelCD, ModeCD, ComeToCD
from aiogram_renderer.filters import IsModeWithNotCustomHandler
from aiogram_renderer.renderer import Renderer

Expand All @@ -11,10 +13,9 @@
RESERVED_CONTAIN_CALLBACKS = ("__mode__", "__dpanel__", "__cometo__")


@router.callback_query(F.data.startswith("__cometo__"))
async def come_to_window(callback: CallbackQuery, renderer: Renderer):
open_state = callback.data.split(":")[1] + ":" + callback.data.split(":")[2]
await renderer.edit(window=open_state, chat_id=callback.message.chat.id, message_id=callback.message.message_id)
@router.callback_query(ComeToCD.filter())
async def come_to_window(callback: CallbackQuery, callback_data: ComeToCD, renderer: Renderer):
await renderer.edit(window=f"{callback_data.group}:{callback_data.state}", chat_id=callback.message.chat.id, message_id=callback.message.message_id)


@router.callback_query(F.data == "__disable__")
Expand All @@ -28,22 +29,19 @@ async def delete_callback_message(callback: CallbackQuery):


@router.callback_query(IsModeWithNotCustomHandler())
async def update_mode(callback: CallbackQuery, state: FSMContext, renderer: Renderer):
mode_name = callback.data.replace("__mode__:", "")
async def update_mode(callback: CallbackQuery, callback_data: ModeCD, state: FSMContext, renderer: Renderer):
# Переключаем режим
await renderer.bot_modes.update_mode(mode=mode_name)
await renderer.bot_modes.update_mode(mode=callback_data.name)
# Для InilineButtonMode бот просто отредактирует окно
await renderer.edit(window=await state.get_state(),
chat_id=callback.message.chat.id,
message_id=callback.message.message_id)


@router.callback_query(F.data.startswith("__dpanel__"))
async def switch_dynamic_panel_page(callback: CallbackQuery, state: FSMContext, renderer: Renderer):
page = int(callback.data.split(":")[1])
panel_name = callback.data.split(":")[2]
@router.callback_query(DPanelCD.filter())
async def switch_dynamic_panel_page(callback: CallbackQuery, callback_data: DPanelCD, state: FSMContext, renderer: Renderer):
message = callback.message
w_state = await state.get_state()

await renderer._switch_dynamic_panel_page(name=panel_name, page=page)
await renderer._switch_dynamic_panel_page(name=callback_data.panel_name, page=callback_data.page)
await renderer.edit(window=w_state, chat_id=message.chat.id, message_id=message.message_id)
27 changes: 13 additions & 14 deletions aiogram_renderer/middlewares.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
from aiogram import types
from aiogram import BaseMiddleware
from typing import Any, Callable

from aiogram import BaseMiddleware
from aiogram.fsm.context import FSMContext
from aiogram.types import Message

from .bot_mode import BotModes, BotMode
from .renderer import Renderer
from .window import Window
Expand All @@ -12,16 +14,13 @@ def __init__(self, windows: list[Window] = None, modes: list[BotMode] = None):
self.windows = windows
self.modes = modes

async def __call__(self, handler: Callable, event: types.Message, data: dict[str, Any]) -> Any:
# Если есть FSMContext то передаем его в renderer и bot_modes
for key, value in data.items():
if isinstance(value, FSMContext):
bot_modes = BotModes(*self.modes, fsm=value) if (self.modes is not None) else None
renderer = Renderer(bot=event.bot, fsm=value, windows=self.windows, bot_modes=bot_modes)
data["renderer"] = renderer
result = await handler(event, data)
del renderer
return result
async def __call__(self, handler: Callable, event: Message, data: dict[str, Any]) -> Any:
# Если есть FSMContext, то передаем его в renderer и bot_modes
if (fsm := data.get('state')) is not None:
data["renderer"] = Renderer(
bot=event.bot,
fsm=fsm,
windows=self.windows,
bot_modes=BotModes(*self.modes, fsm=fsm) if (self.modes is not None) else None)

result = await handler(event, data)
return result
return await handler(event, data)
Loading