feat(global): init structure
This commit is contained in:
43
src/bot/__init__.py
Normal file
43
src/bot/__init__.py
Normal file
@@ -0,0 +1,43 @@
|
||||
import contextlib
|
||||
|
||||
from rich import print
|
||||
|
||||
|
||||
async def runner():
|
||||
from dishka.integrations.aiogram import setup_dishka
|
||||
|
||||
from dependencies import container
|
||||
from utils.db import init_db
|
||||
|
||||
from . import handlers
|
||||
from .common import bot, dp
|
||||
from .modules.error import on_error
|
||||
|
||||
await init_db()
|
||||
|
||||
dp.error.register(on_error)
|
||||
dp.include_routers(handlers.router)
|
||||
|
||||
setup_dishka(container, dp, auto_inject=True)
|
||||
|
||||
await bot.delete_webhook(drop_pending_updates=True)
|
||||
await dp.start_polling(bot)
|
||||
|
||||
|
||||
def plugins():
|
||||
from rich import traceback
|
||||
|
||||
traceback.install(show_locals=True)
|
||||
|
||||
|
||||
def main():
|
||||
import asyncio
|
||||
|
||||
plugins()
|
||||
|
||||
print("Starting...")
|
||||
|
||||
with contextlib.suppress(KeyboardInterrupt):
|
||||
asyncio.run(runner())
|
||||
|
||||
print("[red]Stopped.[/]")
|
||||
4
src/bot/__main__.py
Normal file
4
src/bot/__main__.py
Normal file
@@ -0,0 +1,4 @@
|
||||
from . import main
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
16
src/bot/common.py
Normal file
16
src/bot/common.py
Normal file
@@ -0,0 +1,16 @@
|
||||
from aiogram import Bot, Dispatcher
|
||||
from aiogram.client.default import DefaultBotProperties
|
||||
from aiogram.fsm.storage.memory import MemoryStorage
|
||||
|
||||
from utils import env
|
||||
from utils.config import config, dconfig
|
||||
|
||||
bot = Bot(
|
||||
token=env.bot.token.get_secret_value(),
|
||||
default=DefaultBotProperties(parse_mode="HTML"),
|
||||
)
|
||||
storage = MemoryStorage()
|
||||
dp = Dispatcher(storage=storage)
|
||||
|
||||
|
||||
__all__ = ["bot", "dp", "config", "dconfig"]
|
||||
1
src/bot/filters/__init__.py
Normal file
1
src/bot/filters/__init__.py
Normal file
@@ -0,0 +1 @@
|
||||
from .admin import Admin
|
||||
17
src/bot/filters/admin.py
Normal file
17
src/bot/filters/admin.py
Normal file
@@ -0,0 +1,17 @@
|
||||
from aiogram.filters import BaseFilter
|
||||
from aiogram.types import Message
|
||||
|
||||
from bot.common import config
|
||||
|
||||
|
||||
class Admin(BaseFilter):
|
||||
def __init__(self, notify: bool = True):
|
||||
self.notify = notify
|
||||
|
||||
async def __call__(self, message: Message):
|
||||
try:
|
||||
return message.from_user.id in config.bot.admins
|
||||
except TypeError:
|
||||
if self.notify:
|
||||
await message.answer("Вы не админ!")
|
||||
return False
|
||||
14
src/bot/handlers/__init__.py
Normal file
14
src/bot/handlers/__init__.py
Normal file
@@ -0,0 +1,14 @@
|
||||
from aiogram import Router
|
||||
|
||||
from . import (
|
||||
initialize,
|
||||
start,
|
||||
)
|
||||
|
||||
router = Router()
|
||||
|
||||
|
||||
router.include_routers(
|
||||
start.router,
|
||||
initialize.router,
|
||||
)
|
||||
3
src/bot/handlers/initialize/__init__.py
Normal file
3
src/bot/handlers/initialize/__init__.py
Normal file
@@ -0,0 +1,3 @@
|
||||
from .initializer import router
|
||||
|
||||
__all__ = ["router"]
|
||||
17
src/bot/handlers/initialize/initializer.py
Normal file
17
src/bot/handlers/initialize/initializer.py
Normal file
@@ -0,0 +1,17 @@
|
||||
from aiogram import Bot, Router, types
|
||||
from rich import print
|
||||
|
||||
router = Router()
|
||||
|
||||
|
||||
@router.startup()
|
||||
async def startup(bot: Bot):
|
||||
await bot.set_my_commands(
|
||||
[types.BotCommand(command="/start", description="Запустить бота")]
|
||||
)
|
||||
print(f"[green]Started as[/] @{(await bot.me()).username}")
|
||||
|
||||
|
||||
@router.shutdown()
|
||||
async def shutdown():
|
||||
print("Shutting down bot...")
|
||||
3
src/bot/handlers/start/__init__.py
Normal file
3
src/bot/handlers/start/__init__.py
Normal file
@@ -0,0 +1,3 @@
|
||||
from .start import router
|
||||
|
||||
__all__ = ["router"]
|
||||
9
src/bot/handlers/start/start.py
Normal file
9
src/bot/handlers/start/start.py
Normal file
@@ -0,0 +1,9 @@
|
||||
from aiogram import Router, types
|
||||
from aiogram.filters import CommandStart
|
||||
|
||||
router = Router()
|
||||
|
||||
|
||||
@router.message(CommandStart())
|
||||
async def on_start(message: types.Message):
|
||||
await message.reply("hewo everynyan")
|
||||
0
src/bot/modules/__init__.py
Normal file
0
src/bot/modules/__init__.py
Normal file
3
src/bot/modules/error/__init__.py
Normal file
3
src/bot/modules/error/__init__.py
Normal file
@@ -0,0 +1,3 @@
|
||||
from .handler import on_error
|
||||
|
||||
__all__ = ["on_error"]
|
||||
41
src/bot/modules/error/handler.py
Normal file
41
src/bot/modules/error/handler.py
Normal file
@@ -0,0 +1,41 @@
|
||||
from aiogram import Bot
|
||||
from aiogram.dispatcher import router as s_router
|
||||
from aiogram.types.error_event import ErrorEvent
|
||||
from rich.traceback import Traceback
|
||||
|
||||
from utils.logging import console
|
||||
|
||||
|
||||
async def on_error(event: ErrorEvent, bot: Bot):
|
||||
import base64
|
||||
import os
|
||||
|
||||
error_id = base64.urlsafe_b64encode(os.urandom(6)).decode()
|
||||
|
||||
traceback = Traceback.from_exception(
|
||||
type(event.exception),
|
||||
event.exception,
|
||||
event.exception.__traceback__,
|
||||
show_locals=True,
|
||||
suppress=[s_router],
|
||||
)
|
||||
|
||||
if event.update.chosen_inline_result:
|
||||
await bot.edit_message_caption(
|
||||
inline_message_id=event.update.chosen_inline_result.inline_message_id,
|
||||
caption=f"💔 <b>ERROR</b> occurred. Use this code to search in logs: "
|
||||
f"<code>{error_id}</code>",
|
||||
parse_mode="HTML",
|
||||
)
|
||||
|
||||
if event.update.message:
|
||||
await event.update.message.answer(
|
||||
text=f"💔 <b>ERROR</b> occurred. Use this code to search in logs: "
|
||||
f"<code>{error_id}</code>",
|
||||
parse_mode="HTML",
|
||||
)
|
||||
|
||||
console.print(f"[red]{error_id} occurred[/]")
|
||||
console.print(event)
|
||||
console.print(traceback)
|
||||
console.print(f"-{error_id} occurred-")
|
||||
1
src/bot/modules/paginator/__init__.py
Normal file
1
src/bot/modules/paginator/__init__.py
Normal file
@@ -0,0 +1 @@
|
||||
from .paginator import Paginator
|
||||
178
src/bot/modules/paginator/paginator.py
Normal file
178
src/bot/modules/paginator/paginator.py
Normal file
@@ -0,0 +1,178 @@
|
||||
from itertools import islice
|
||||
from typing import Any, Callable, Coroutine, Iterable, Iterator
|
||||
|
||||
from aiogram import Dispatcher, F, Router, types
|
||||
from aiogram.fsm.context import FSMContext
|
||||
from aiogram.types import CallbackQuery
|
||||
from aiogram.utils.keyboard import InlineKeyboardBuilder
|
||||
|
||||
|
||||
class Paginator:
|
||||
def __init__(
|
||||
self,
|
||||
data: (
|
||||
types.InlineKeyboardMarkup
|
||||
| Iterable[types.InlineKeyboardButton]
|
||||
| Iterable[Iterable[types.InlineKeyboardButton]]
|
||||
| InlineKeyboardBuilder
|
||||
),
|
||||
before_data: list[list[types.InlineKeyboardButton]] = None,
|
||||
after_data: list[list[types.InlineKeyboardButton]] = None,
|
||||
state: FSMContext = None,
|
||||
callback_startswith: str = "page_",
|
||||
size: int = 8,
|
||||
page_separator: str = "/",
|
||||
dp: Dispatcher | Router | None = None,
|
||||
):
|
||||
"""
|
||||
Example: paginator = Paginator(data=kb, size=5)
|
||||
|
||||
:param data: An iterable object that stores an InlineKeyboardButton.
|
||||
:param callback_startswith: What should callback_data begin with in handler pagination. Default = 'page_'.
|
||||
:param size: Number of lines per page. Default = 8.
|
||||
:param state: Current state.
|
||||
:param page_separator: Separator for page numbers. Default = '/'.
|
||||
"""
|
||||
self.dp = dp
|
||||
self.page_separator = page_separator
|
||||
self._state = state
|
||||
self._size = size
|
||||
self._startswith = callback_startswith
|
||||
self._before_data = before_data or []
|
||||
self._after_data = after_data or []
|
||||
if isinstance(data, types.InlineKeyboardMarkup):
|
||||
self._list_kb = list(self._chunk(it=data.inline_keyboard, size=self._size))
|
||||
elif isinstance(data, Iterable):
|
||||
self._list_kb = list(self._chunk(it=list(data), size=self._size))
|
||||
elif isinstance(data, InlineKeyboardBuilder):
|
||||
self._list_kb = list(self._chunk(it=data.export(), size=self._size))
|
||||
else:
|
||||
raise ValueError(f"{data} is not valid data")
|
||||
|
||||
"""
|
||||
Class for pagination's in aiogram inline keyboards
|
||||
"""
|
||||
|
||||
def __call__(self, current_page=0, *args, **kwargs) -> types.InlineKeyboardMarkup:
|
||||
"""
|
||||
Example:
|
||||
|
||||
await message.answer(
|
||||
text='Some menu',
|
||||
reply_markup=paginator()
|
||||
)
|
||||
|
||||
:return: InlineKeyboardMarkup
|
||||
"""
|
||||
if current_page >= len(self._list_kb):
|
||||
current_page = 0
|
||||
|
||||
_list_current_page = self._list_kb[current_page]
|
||||
|
||||
paginations = self._get_paginator(
|
||||
counts=len(self._list_kb),
|
||||
page=current_page,
|
||||
page_separator=self.page_separator,
|
||||
startswith=self._startswith,
|
||||
)
|
||||
keyboard = types.InlineKeyboardMarkup(
|
||||
inline_keyboard=self._before_data
|
||||
+ [
|
||||
*_list_current_page,
|
||||
paginations,
|
||||
]
|
||||
+ self._after_data
|
||||
)
|
||||
|
||||
if self.dp:
|
||||
self.paginator_handler()
|
||||
|
||||
return keyboard
|
||||
|
||||
@staticmethod
|
||||
def _get_page(call: types.CallbackQuery) -> int:
|
||||
"""
|
||||
:param call: CallbackQuery in paginator handler.
|
||||
:return: Current page.
|
||||
"""
|
||||
return int(call.data[-1])
|
||||
|
||||
@staticmethod
|
||||
def _chunk(it, size) -> Iterator[tuple[Any, ...]]:
|
||||
"""
|
||||
:param it: Source iterable object.
|
||||
:param size: Chunk size.
|
||||
:return: Iterator chunks pages.
|
||||
"""
|
||||
it = iter(it)
|
||||
return iter(lambda: tuple(islice(it, size)), ())
|
||||
|
||||
@staticmethod
|
||||
def _get_paginator(
|
||||
counts: int, page: int, page_separator: str = "/", startswith: str = "page_"
|
||||
) -> list[types.InlineKeyboardButton]:
|
||||
"""
|
||||
:param counts: Counts total buttons.
|
||||
:param page: Current page.
|
||||
:param page_separator: Separator for page numbers. Default = '/'.
|
||||
:return: Page control line buttons.
|
||||
"""
|
||||
counts -= 1
|
||||
|
||||
paginations = []
|
||||
|
||||
if page > 0:
|
||||
paginations.append(
|
||||
types.InlineKeyboardButton(text="⏮️️", callback_data=f"{startswith}0")
|
||||
)
|
||||
paginations.append(
|
||||
types.InlineKeyboardButton(
|
||||
text="⬅️", callback_data=f"{startswith}{page - 1}"
|
||||
),
|
||||
)
|
||||
paginations.append(
|
||||
types.InlineKeyboardButton(
|
||||
text=f"{page + 1}{page_separator}{counts + 1}", callback_data="pass"
|
||||
),
|
||||
)
|
||||
if counts > page:
|
||||
paginations.append(
|
||||
types.InlineKeyboardButton(
|
||||
text="➡️", callback_data=f"{startswith}{page + 1}"
|
||||
)
|
||||
)
|
||||
paginations.append(
|
||||
types.InlineKeyboardButton(
|
||||
text="⏭️", callback_data=f"{startswith}{counts}"
|
||||
)
|
||||
)
|
||||
return paginations
|
||||
|
||||
def paginator_handler(
|
||||
self,
|
||||
) -> tuple[Callable[[CallbackQuery, FSMContext], Coroutine[Any, Any, None]], Any]:
|
||||
"""
|
||||
Example:
|
||||
|
||||
args, kwargs = paginator.paginator_handler()
|
||||
|
||||
dp.register_callback_query_handler(*args, **kwargs)
|
||||
|
||||
:return: Data for register handler pagination.
|
||||
"""
|
||||
|
||||
async def _page(call: types.CallbackQuery, state: FSMContext):
|
||||
page = self._get_page(call)
|
||||
|
||||
await call.message.edit_reply_markup(
|
||||
reply_markup=self.__call__(current_page=page)
|
||||
)
|
||||
await state.update_data({f"last_page_{self._startswith}": page})
|
||||
|
||||
if not self.dp:
|
||||
return _page, F.data.startswith(self._startswith)
|
||||
else:
|
||||
self.dp.callback_query.register(
|
||||
_page,
|
||||
F.data.startswith(self._startswith),
|
||||
)
|
||||
0
src/bot/states/__init__.py
Normal file
0
src/bot/states/__init__.py
Normal file
Reference in New Issue
Block a user