diff --git a/alembic.ini b/alembic.ini new file mode 100644 index 0000000..56d1d18 --- /dev/null +++ b/alembic.ini @@ -0,0 +1,114 @@ +# A generic, single database configuration. + +[alembic] +# path to migration scripts +script_location = alembic + +# template used to generate migration file names; The default value is %%(rev)s_%%(slug)s +# Uncomment the line below if you want the files to be prepended with date and time +file_template = %%(year)d_%%(month).2d_%%(day).2d_%%(hour).2d%%(minute).2d-%%(rev)s_%%(slug)s + +# sys.path path, will be prepended to sys.path if present. +# defaults to the current working directory. +prepend_sys_path = . + +# timezone to use when rendering the date within the migration file +# as well as the filename. +# If specified, requires the python>=3.9 or backports.zoneinfo library. +# Any required deps can installed by adding `alembic[tz]` to the pip requirements +# string value is passed to ZoneInfo() +# leave blank for localtime +# timezone = + +# max length of characters to apply to the +# "slug" field +# truncate_slug_length = 40 + +# set to 'true' to run the environment during +# the 'revision' command, regardless of autogenerate +# revision_environment = false + +# set to 'true' to allow .pyc and .pyo files without +# a source .py file to be detected as revisions in the +# versions/ directory +# sourceless = false + +# version location specification; This defaults +# to alembic/versions. When using multiple version +# directories, initial revisions must be specified with --version-path. +# The path separator used here should be the separator specified by "version_path_separator" below. +# version_locations = %(here)s/bar:%(here)s/bat:alembic/versions + +# version path separator; As mentioned above, this is the character used to split +# version_locations. The default within new alembic.ini files is "os", which uses os.pathsep. +# If this key is omitted entirely, it falls back to the legacy behavior of splitting on spaces and/or commas. +# Valid values for version_path_separator are: +# +# version_path_separator = : +# version_path_separator = ; +# version_path_separator = space +version_path_separator = os # Use os.pathsep. Default configuration used for new projects. + +# set to 'true' to search source files recursively +# in each "version_locations" directory +# new in Alembic version 1.10 +# recursive_version_locations = false + +# the output encoding used when revision files +# are written from script.py.mako +# output_encoding = utf-8 + +# sqlalchemy.url = driver://user:pass@localhost/dbname + + +[post_write_hooks] +# post_write_hooks defines scripts or Python functions that are run +# on newly generated revision scripts. See the documentation for further +# detail and examples + +# format using "black" - use the console_scripts runner, against the "black" entrypoint +hooks = black +black.type = console_scripts +black.entrypoint = black +black.options = -l 79 REVISION_SCRIPT_FILENAME + +# lint with attempts to fix using "ruff" - use the exec runner, execute a binary +# hooks = ruff +# ruff.type = exec +# ruff.executable = %(here)s/.venv/bin/ruff +# ruff.options = --fix REVISION_SCRIPT_FILENAME + +# Logging configuration +[loggers] +keys = root,sqlalchemy,alembic + +[handlers] +keys = console + +[formatters] +keys = generic + +[logger_root] +level = WARN +handlers = console +qualname = + +[logger_sqlalchemy] +level = WARN +handlers = +qualname = sqlalchemy.engine + +[logger_alembic] +level = INFO +handlers = +qualname = alembic + +[handler_console] +class = StreamHandler +args = (sys.stderr,) +level = NOTSET +formatter = generic + +[formatter_generic] +format = %(levelname)-5.5s [%(name)s] %(message)s +datefmt = %H:%M:%S diff --git a/alembic/README b/alembic/README new file mode 100644 index 0000000..e0d0858 --- /dev/null +++ b/alembic/README @@ -0,0 +1 @@ +Generic single-database configuration with an async dbapi. \ No newline at end of file diff --git a/alembic/env.py b/alembic/env.py new file mode 100644 index 0000000..6f045bb --- /dev/null +++ b/alembic/env.py @@ -0,0 +1,94 @@ +import asyncio +from logging.config import fileConfig + +from sqlalchemy import pool +from sqlalchemy.engine import Connection +from sqlalchemy.ext.asyncio import async_engine_from_config + +from alembic import context + +from app.models import Base +from app.settings import secrets + +# this is the Alembic Config object, which provides +# access to the values within the .ini file in use. +config = context.config +config.set_main_option("sqlalchemy.url", secrets.db_url) + +# Interpret the config file for Python logging. +# This line sets up loggers basically. +if config.config_file_name is not None: + fileConfig(config.config_file_name) + +# add your model's MetaData object here +# for 'autogenerate' support +# from myapp import mymodel +# target_metadata = mymodel.Base.metadata +target_metadata = Base.metadata + + +# other values from the config, defined by the needs of env.py, +# can be acquired: +# my_important_option = config.get_main_option("my_important_option") +# ... etc. + + +def run_migrations_offline() -> None: + """Run migrations in 'offline' mode. + + This configures the context with just a URL + and not an Engine, though an Engine is acceptable + here as well. By skipping the Engine creation + we don't even need a DBAPI to be available. + + Calls to context.execute() here emit the given string to the + script output. + + """ + url = config.get_main_option("sqlalchemy.url") + context.configure( + url=url, + target_metadata=target_metadata, + literal_binds=True, + dialect_opts={"paramstyle": "named"}, + ) + + with context.begin_transaction(): + context.run_migrations() + + +def do_run_migrations(connection: Connection) -> None: + context.configure(connection=connection, target_metadata=target_metadata) + + with context.begin_transaction(): + context.run_migrations() + + +async def run_async_migrations() -> None: + """In this scenario we need to create an Engine + and associate a connection with the context. + + """ + + connectable = async_engine_from_config( + config.get_section(config.config_ini_section, {}), + prefix="sqlalchemy.", + poolclass=pool.NullPool, + ) + + async with connectable.connect() as connection: + await connection.run_sync(do_run_migrations) + + await connectable.dispose() + + +def run_migrations_online() -> None: + """Run migrations in 'online' mode.""" + + asyncio.run(run_async_migrations()) + + +if context.is_offline_mode(): + run_migrations_offline() +else: + run_migrations_online() diff --git a/alembic/script.py.mako b/alembic/script.py.mako new file mode 100644 index 0000000..fbc4b07 --- /dev/null +++ b/alembic/script.py.mako @@ -0,0 +1,26 @@ +"""${message} + +Revision ID: ${up_revision} +Revises: ${down_revision | comma,n} +Create Date: ${create_date} + +""" +from typing import Sequence, Union + +from alembic import op +import sqlalchemy as sa +${imports if imports else ""} + +# revision identifiers, used by Alembic. +revision: str = ${repr(up_revision)} +down_revision: Union[str, None] = ${repr(down_revision)} +branch_labels: Union[str, Sequence[str], None] = ${repr(branch_labels)} +depends_on: Union[str, Sequence[str], None] = ${repr(depends_on)} + + +def upgrade() -> None: + ${upgrades if upgrades else "pass"} + + +def downgrade() -> None: + ${downgrades if downgrades else "pass"} diff --git a/app/callbacks/__init__.py b/app/callbacks/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/app/callbacks/callback_docker.py b/app/callbacks/callback_docker.py new file mode 100644 index 0000000..aba09a5 --- /dev/null +++ b/app/callbacks/callback_docker.py @@ -0,0 +1,10 @@ +from aiogram.filters.callback_data import CallbackData + + +class ContainerCallback(CallbackData, prefix="container"): + name: str + + +class ActionCallback(CallbackData, prefix="action"): + name: str + action: str diff --git a/app/callbacks/callback_favorites.py b/app/callbacks/callback_favorites.py new file mode 100644 index 0000000..bf599a8 --- /dev/null +++ b/app/callbacks/callback_favorites.py @@ -0,0 +1,13 @@ +from aiogram.filters.callback_data import CallbackData + + +class FavoriteCallback(CallbackData, prefix="favorite"): + user_id: int + + +class AddFavoriteCallback(FavoriteCallback, prefix="add"): + pass + + +class DelFavoriteCallback(FavoriteCallback, prefix="del"): + pass diff --git a/app/handlers/docker_commands.py b/app/handlers/docker_commands.py index 340a88c..7497c82 100644 --- a/app/handlers/docker_commands.py +++ b/app/handlers/docker_commands.py @@ -2,47 +2,42 @@ import subprocess from aiogram.types import Message, CallbackQuery +from app import views +from app.callbacks.callback_docker import ContainerCallback, ActionCallback from app.keyboards.docker_keyboards import container_names_keyboard, container_actions_keyboard +from app.utils.text_splitter import split_text async def get_containers(message: Message): - sub = subprocess.check_output("docker ps -a").decode() + sub = subprocess.check_output("docker ps -a", shell=True).decode() + messages = split_text(sub) + + for m in messages: + await message.answer( + text=f"
{m}
",
+ parse_mode="HTML",
+ )
+
keyboard = container_names_keyboard(sub)
- await message.answer(
- text=f"{sub}
",
- parse_mode="HTML",
- reply_markup=keyboard.as_markup()
- )
+ await message.answer(text="Выберите контейнер:", reply_markup=keyboard.as_markup())
-async def container_actions(call: CallbackQuery):
- name = call.data.split("_")[-1]
+async def container_actions(callback: CallbackQuery, callback_data: ContainerCallback):
+ name = callback_data.name
keyboard = container_actions_keyboard(name)
- await call.message.answer(
+ await callback.message.answer(
text=f"Выберите действие для контейнера {name}",
parse_mode="HTML",
reply_markup=keyboard.as_markup()
)
-async def do_container_action(call: CallbackQuery):
- _, action, name = call.data.split("_")
- match action:
- case "start":
- subprocess.run(f"docker start {name}")
- message = f"Контейнер {name} успешно запущен"
- case "stop":
- subprocess.run(f"docker stop {name}")
- message = f"Контейнер {name} успешно остановлен"
- case "restart":
- subprocess.run(f"docker restart {name}")
- message = f"Контейнер {name} успешно перезапущен"
- case "delete":
- subprocess.run(f"docker rm -f {name}")
- message = f"Контейнер {name} успешно удалён"
- case _:
- message = f"Произошла необъяснимая ошибка"
+async def do_container_action(callback: CallbackQuery, callback_data: ActionCallback):
+ action, name = callback_data.name, callback_data.action
- await call.message.answer(
+ subprocess.run(f"docker {action} {name}", shell=True)
+ message = views.actions.get(action).format(name)
+
+ await callback.message.answer(
text=message,
)
diff --git a/app/handlers/events.py b/app/handlers/events.py
index 8d1ad70..2303e8d 100644
--- a/app/handlers/events.py
+++ b/app/handlers/events.py
@@ -1,12 +1,12 @@
-from app.settings import bot, Secrets
+from app.settings import bot, secrets
from app.utils.commands import set_commands
from app import views
async def start_bot():
await set_commands(bot)
- await bot.send_message(Secrets.admin_id, views.start_bot_message())
+ await bot.send_message(secrets.admin_id, views.start_bot_message())
async def stop_bot():
- await bot.send_message(Secrets.admin_id, views.stop_bot_message())
+ await bot.send_message(secrets.admin_id, views.stop_bot_message())
diff --git a/app/handlers/favorites.py b/app/handlers/favorites.py
index e69de29..5d3cbbb 100644
--- a/app/handlers/favorites.py
+++ b/app/handlers/favorites.py
@@ -0,0 +1,60 @@
+from aiogram.fsm.context import FSMContext
+from aiogram.types import Message, CallbackQuery
+
+from app.callbacks.callback_favorites import AddFavoriteCallback, DelFavoriteCallback
+from app.keyboards.favorites_keyboards import favorite_list, add_favorite_inline, add_del_favorite_inline
+from app.utils.db_actions import get_user, create_user, get_favorites, add_favorite, del_favorite
+from app.utils.statesform import FavoritesCommandsSteps
+
+
+async def favorites_command(message: Message):
+ user = await get_user(message.from_user.id)
+ if not user:
+ user = await create_user(message.from_user.id)
+
+ favorites = await get_favorites(user.id)
+
+ if not favorites:
+ inline_keyboard = await add_favorite_inline(user)
+ await message.answer("Нет команд в избранном.", reply_markup=inline_keyboard)
+ else:
+ inline_keyboard = await add_del_favorite_inline(user)
+ await message.answer("Выберите действие:", reply_markup=inline_keyboard)
+ keyboard = await favorite_list(favorites)
+ await message.answer("Выберите команду из списка ниже:", reply_markup=keyboard)
+
+
+async def add_favorite_callback(callback: CallbackQuery, callback_data: AddFavoriteCallback, state: FSMContext):
+ await callback.message.answer("Введите команду.")
+ await state.set_data({"user_id": callback_data.user_id})
+ await state.set_state(FavoritesCommandsSteps.ADD)
+
+
+async def add_favorite_state(message: Message, state: FSMContext):
+ data = await state.get_data()
+ new_command = await add_favorite(user_id=data.get("user_id"), command=message.text)
+ await message.answer(f"Добавлена команда: {new_command.command}")
+ await state.clear()
+
+
+async def del_favorite_callback(callback: CallbackQuery, callback_data: DelFavoriteCallback, state: FSMContext):
+ favorites = await get_favorites(callback_data.user_id)
+ favorites_text = "\n".join([f"{i}: {favorite.command}" for i, favorite in enumerate(favorites)])
+ await callback.message.answer(f"Введите номер команды для удаления:\n{favorites_text}")
+ await state.set_data({"favorites": favorites})
+ await state.set_state(FavoritesCommandsSteps.DEL)
+
+
+async def del_favorite_state(message: Message, state: FSMContext):
+ data = await state.get_data()
+
+ try:
+ choice = int(message.text)
+ except ValueError:
+ await message.answer("Введите число!")
+ await state.set_state(FavoritesCommandsSteps.DEL)
+ else:
+ favorite = data.get("favorites")[choice]
+ await del_favorite(favorite.id)
+ await message.answer("Команда успешно удалена.")
+ await state.clear()
diff --git a/app/handlers/multiply_commands.py b/app/handlers/multiply_commands.py
index 3a7d138..de2cdef 100644
--- a/app/handlers/multiply_commands.py
+++ b/app/handlers/multiply_commands.py
@@ -6,6 +6,7 @@ from aiogram.types import Message
from app import views
from app.utils.statesform import ExecuteCommandsSteps
+from app.utils.text_splitter import split_text
async def multiply_commands(message: Message, state: FSMContext):
@@ -34,6 +35,9 @@ async def execute_commands(message: Message, state: FSMContext):
except subprocess.CalledProcessError as e:
res = views.subprocess_error(e)
- await message.answer(views.user_command(res), parse_mode="HTML")
+ messages = split_text(res)
+
+ for m in messages:
+ await message.answer(views.user_command(m), parse_mode="HTML")
await state.set_state(ExecuteCommandsSteps.EXECUTE)
diff --git a/app/handlers/single_command.py b/app/handlers/single_command.py
index a134746..aa11a93 100644
--- a/app/handlers/single_command.py
+++ b/app/handlers/single_command.py
@@ -3,10 +3,15 @@ import subprocess
from aiogram.types import Message
from app import views
+from app.utils.text_splitter import split_text
async def execute_command(message: Message):
user_command = message.text.split()[1:]
sub = subprocess.check_output(user_command, shell=True)
res = sub.decode().replace("&", "&").replace("<", "<").replace(">", ">")
- await message.answer(views.user_command(res), parse_mode="HTML")
+
+ messages = split_text(res)
+
+ for m in messages:
+ await message.answer(views.user_command(m), parse_mode="HTML")
diff --git a/app/keyboards/docker_keyboards.py b/app/keyboards/docker_keyboards.py
index 224a9ff..013b7fa 100644
--- a/app/keyboards/docker_keyboards.py
+++ b/app/keyboards/docker_keyboards.py
@@ -1,16 +1,17 @@
from aiogram.types import InlineKeyboardButton
from aiogram.utils.keyboard import InlineKeyboardBuilder
+from app.callbacks.callback_docker import ContainerCallback, ActionCallback
+
def container_names_keyboard(stdout: str):
container_names = [line.split(" ")[-1].strip() for line in stdout.splitlines()[1:]]
builder = InlineKeyboardBuilder()
for name in container_names:
- data = f"container_{name}"
builder.add(
InlineKeyboardButton(
text=name,
- callback_data=data,
+ callback_data=ContainerCallback(name=name).pack(),
)
)
builder.adjust(1)
@@ -23,19 +24,19 @@ def container_actions_keyboard(name: str):
builder.add(InlineKeyboardButton(
text="Запустить контейнер",
- callback_data=f"action_start_{name}",
+ callback_data=ActionCallback(name=name, action="start").pack(),
))
builder.add(InlineKeyboardButton(
text="Остановить контейнер",
- callback_data=f"action_stop_{name}",
+ callback_data=ActionCallback(name=name, action="stop").pack(),
))
builder.add(InlineKeyboardButton(
text="Перезапустить контейнер",
- callback_data=f"action_restart_{name}",
+ callback_data=ActionCallback(name=name, action="restart").pack(),
))
builder.add(InlineKeyboardButton(
text="Удалить контейнер",
- callback_data=f"action_delete_{name}",
+ callback_data=ActionCallback(name=name, action="delete").pack(),
))
builder.adjust(1)
diff --git a/app/keyboards/favorites_keyboards.py b/app/keyboards/favorites_keyboards.py
new file mode 100644
index 0000000..35f5df0
--- /dev/null
+++ b/app/keyboards/favorites_keyboards.py
@@ -0,0 +1,52 @@
+from aiogram.types import KeyboardButton, InlineKeyboardButton
+from aiogram.utils.keyboard import ReplyKeyboardBuilder, InlineKeyboardBuilder
+
+from app.callbacks.callback_favorites import AddFavoriteCallback, DelFavoriteCallback
+from app.schemas.favorites_schema import FavoritesSchemaOutput
+from app.schemas.user_schema import UserSchemaOutput
+
+
+async def favorite_list(favorites: list[FavoritesSchemaOutput]):
+ builder = ReplyKeyboardBuilder()
+
+ for favorite in favorites:
+ builder.add(
+ KeyboardButton(text=f"/command {favorite.command}")
+ )
+
+ builder.adjust(2)
+ return builder.as_markup(resize_keyboard=True, one_time_keyboard=True)
+
+
+async def add_favorite_inline(user: UserSchemaOutput):
+ builder = InlineKeyboardBuilder()
+
+ builder.add(
+ InlineKeyboardButton(
+ text="Добавить команду",
+ callback_data=AddFavoriteCallback(user_id=user.id).pack()
+ )
+ )
+
+ builder.adjust(1)
+ return builder.as_markup()
+
+
+async def add_del_favorite_inline(user: UserSchemaOutput):
+ builder = InlineKeyboardBuilder()
+
+ builder.add(
+ InlineKeyboardButton(
+ text="Добавить команду",
+ callback_data=AddFavoriteCallback(user_id=user.id).pack()
+ )
+ )
+ builder.add(
+ InlineKeyboardButton(
+ text="Удалить команду",
+ callback_data=DelFavoriteCallback(user_id=user.id).pack()
+ )
+ )
+
+ builder.adjust(1)
+ return builder.as_markup()
diff --git a/app/middlewares/__init__.py b/app/middlewares/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/app/middlewares/admin_middleware.py b/app/middlewares/admin_middleware.py
new file mode 100644
index 0000000..31555cd
--- /dev/null
+++ b/app/middlewares/admin_middleware.py
@@ -0,0 +1,18 @@
+from typing import Callable, Dict, Any, Awaitable
+
+from aiogram import BaseMiddleware
+from aiogram.types import TelegramObject, User
+
+from app.settings import secrets
+
+
+class AdminMiddleware(BaseMiddleware):
+ async def __call__(
+ self,
+ handler: Callable[[TelegramObject, Dict[str, Any]], Awaitable[Any]],
+ event: TelegramObject,
+ data: Dict[str, Any]
+ ) -> Any:
+ user: User = data.get("event_from_user")
+ if user.id == secrets.admin_id:
+ return await handler(event, data)
diff --git a/app/models/__init__.py b/app/models/__init__.py
new file mode 100644
index 0000000..5c1623b
--- /dev/null
+++ b/app/models/__init__.py
@@ -0,0 +1,9 @@
+from .base import Base
+from .user import User
+from .favorites import Favorites
+
+__all__ = [
+ "Base",
+ "User",
+ "Favorites",
+]
diff --git a/app/models/base.py b/app/models/base.py
new file mode 100644
index 0000000..d33df06
--- /dev/null
+++ b/app/models/base.py
@@ -0,0 +1,7 @@
+from sqlalchemy.orm import DeclarativeBase, Mapped, mapped_column
+
+
+class Base(DeclarativeBase):
+ __abstract__ = True
+
+ id: Mapped[int] = mapped_column(primary_key=True, autoincrement=True)
diff --git a/app/models/favorites.py b/app/models/favorites.py
new file mode 100644
index 0000000..88dd188
--- /dev/null
+++ b/app/models/favorites.py
@@ -0,0 +1,17 @@
+from typing import TYPE_CHECKING
+
+from sqlalchemy import String, ForeignKey
+from sqlalchemy.orm import Mapped, mapped_column, relationship
+
+from app.models.base import Base
+
+if TYPE_CHECKING:
+ from .user import User
+
+
+class Favorites(Base):
+ __tablename__ = "favorites"
+
+ command: Mapped[str] = mapped_column(String(200), nullable=False)
+ user_id: Mapped[int] = mapped_column(ForeignKey("user.id"))
+ user: Mapped["User"] = relationship("User", back_populates="favorites")
diff --git a/app/models/user.py b/app/models/user.py
new file mode 100644
index 0000000..a0e33ea
--- /dev/null
+++ b/app/models/user.py
@@ -0,0 +1,18 @@
+from datetime import datetime
+from typing import TYPE_CHECKING
+
+from sqlalchemy import BigInteger, func
+from sqlalchemy.orm import Mapped, mapped_column, relationship
+
+from app.models.base import Base
+
+if TYPE_CHECKING:
+ from .favorites import Favorites
+
+
+class User(Base):
+ __tablename__ = "user"
+
+ telegram_id: Mapped[int] = mapped_column(BigInteger, nullable=False)
+ added_at: Mapped[datetime] = mapped_column(server_default=func.now(), default=datetime.now)
+ favorites: Mapped["Favorites"] = relationship("Favorites", back_populates="user")
diff --git a/app/schemas/__init__.py b/app/schemas/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/app/schemas/favorites_schema.py b/app/schemas/favorites_schema.py
new file mode 100644
index 0000000..e401219
--- /dev/null
+++ b/app/schemas/favorites_schema.py
@@ -0,0 +1,10 @@
+from pydantic import BaseModel
+
+
+class FavoritesSchemaInput(BaseModel):
+ command: str
+ user_id: int
+
+
+class FavoritesSchemaOutput(FavoritesSchemaInput):
+ id: int
diff --git a/app/schemas/user_schema.py b/app/schemas/user_schema.py
new file mode 100644
index 0000000..510dfa5
--- /dev/null
+++ b/app/schemas/user_schema.py
@@ -0,0 +1,12 @@
+from datetime import datetime
+
+from pydantic import BaseModel
+
+
+class UserSchemaInput(BaseModel):
+ telegram_id: int
+
+
+class UserSchemaOutput(UserSchemaInput):
+ id: int
+ added_at: datetime
diff --git a/app/settings.py b/app/settings.py
index aabd21a..2338489 100644
--- a/app/settings.py
+++ b/app/settings.py
@@ -1,16 +1,25 @@
import os
-from dataclasses import dataclass
+from typing import Union
from aiogram import Bot
-from dotenv import load_dotenv
-
-load_dotenv()
+from pydantic import SecretStr
+from pydantic_settings import BaseSettings
+from sqlalchemy.ext.asyncio import create_async_engine, async_sessionmaker
-@dataclass
-class Secrets:
- token: str = os.environ.get("token")
- admin_id: int = os.environ.get("admin_id")
+class Secrets(BaseSettings):
+ token: SecretStr
+ admin_id: Union[SecretStr.get_secret_value, int]
+ db_url: str = "sqlite+aiosqlite:///db.sqlite3"
+
+ class Config:
+ env_file = ".env"
+ env_file_encoding = "utf-8"
-bot = Bot(token=Secrets.token)
+secrets = Secrets()
+
+engine = create_async_engine(url=secrets.db_url)
+sessionmaker = async_sessionmaker(engine, expire_on_commit=False, autocommit=False)
+
+bot = Bot(token=secrets.token.get_secret_value())
diff --git a/app/utils/db_actions.py b/app/utils/db_actions.py
new file mode 100644
index 0000000..481cfb2
--- /dev/null
+++ b/app/utils/db_actions.py
@@ -0,0 +1,50 @@
+from sqlalchemy import select, insert, delete
+
+from app.models import User, Favorites
+from app.schemas.favorites_schema import FavoritesSchemaOutput
+from app.schemas.user_schema import UserSchemaOutput
+from app.settings import sessionmaker
+
+
+async def get_user(telegram_id: int):
+ async with sessionmaker() as session:
+ query = select(User).where(User.telegram_id == telegram_id)
+ result = await session.execute(query)
+ user = result.scalar_one_or_none()
+ if user:
+ return UserSchemaOutput(**user.__dict__)
+ return user
+
+
+async def create_user(telegram_id: int):
+ async with sessionmaker() as session:
+ query = insert(User).values(telegram_id=telegram_id).returning(User.id, User.telegram_id, User.added_at)
+ result = await session.execute(query)
+ await session.commit()
+ user = result.mappings().first()
+ return UserSchemaOutput(**user)
+
+
+async def get_favorites(user_id: int):
+ async with sessionmaker() as session:
+ query = select(Favorites).where(Favorites.user_id == user_id)
+ result = await session.execute(query)
+ favorites = result.scalars().all()
+ return [FavoritesSchemaOutput(**favorite.__dict__) for favorite in favorites]
+
+
+async def add_favorite(user_id: int, command: str):
+ async with sessionmaker() as session:
+ query = insert(Favorites).values(user_id=user_id, command=command).returning(Favorites.id, Favorites.user_id,
+ Favorites.command)
+ result = await session.execute(query)
+ await session.commit()
+ favorite = result.mappings().first()
+ return FavoritesSchemaOutput(**favorite)
+
+
+async def del_favorite(favorite_id: int):
+ async with sessionmaker() as session:
+ query = delete(Favorites).where(Favorites.id == favorite_id)
+ await session.execute(query)
+ await session.commit()
diff --git a/app/utils/statesform.py b/app/utils/statesform.py
index 0e6547f..3794eb6 100644
--- a/app/utils/statesform.py
+++ b/app/utils/statesform.py
@@ -3,3 +3,8 @@ from aiogram.fsm.state import StatesGroup, State
class ExecuteCommandsSteps(StatesGroup):
EXECUTE = State()
+
+
+class FavoritesCommandsSteps(StatesGroup):
+ ADD = State()
+ DEL = State()
\ No newline at end of file
diff --git a/app/utils/text_splitter.py b/app/utils/text_splitter.py
new file mode 100644
index 0000000..6cbb931
--- /dev/null
+++ b/app/utils/text_splitter.py
@@ -0,0 +1,20 @@
+import re
+
+
+def split_text(message: str):
+ messages = []
+ lines = re.split(r"\n", message)
+
+ temp = ""
+
+ for line in lines:
+ if len(temp) + len(line) < 4096:
+ temp += line + "\n"
+ else:
+ messages.append(temp)
+ temp = line + "\n"
+
+ if temp:
+ messages.append(temp)
+
+ return messages
\ No newline at end of file
diff --git a/app/views.py b/app/views.py
index 8633e9e..013312a 100644
--- a/app/views.py
+++ b/app/views.py
@@ -5,6 +5,12 @@ menu = {
"docker_list": "Список Docker-контейнеров",
}
+actions = {
+ "start": "Контейнер {} успешно запущен",
+ "stop": "Контейнер {} успешно остановлен",
+ "restart": "Контейнер {} успешно перезапущен",
+ "delete": "Контейнер {} успешно удалён",
+}
def start_bot_message():
return "Бот запущен"
diff --git a/main.py b/main.py
index 72e5283..df4ddd4 100644
--- a/main.py
+++ b/main.py
@@ -3,18 +3,25 @@ import asyncio
from aiogram import Dispatcher, F
from aiogram.filters import Command
+from app.callbacks.callback_docker import ContainerCallback, ActionCallback
+from app.callbacks.callback_favorites import AddFavoriteCallback, DelFavoriteCallback
from app.handlers.docker_commands import get_containers, container_actions, do_container_action
from app.handlers.events import start_bot, stop_bot
+from app.handlers.favorites import favorites_command, add_favorite_callback, add_favorite_state, del_favorite_callback, \
+ del_favorite_state
from app.handlers.multiply_commands import multiply_commands, execute_commands
from app.handlers.simple import start_command
from app.handlers.single_command import execute_command
+from app.middlewares.admin_middleware import AdminMiddleware
from app.settings import bot
-from app.utils.statesform import ExecuteCommandsSteps
+from app.utils.statesform import ExecuteCommandsSteps, FavoritesCommandsSteps
async def start():
dp = Dispatcher()
+ dp.update.middleware(AdminMiddleware())
+
dp.startup.register(start_bot)
dp.shutdown.register(stop_bot)
@@ -22,11 +29,16 @@ async def start():
dp.message.register(execute_command, Command(commands="command"))
dp.message.register(multiply_commands, Command(commands="multiply_commands"))
dp.message.register(get_containers, Command(commands="docker_list"))
+ dp.message.register(favorites_command, Command(commands="favorites"))
- dp.callback_query.register(container_actions, F.data.startswith("container"))
- dp.callback_query.register(do_container_action, F.data.startswith("action"))
+ dp.callback_query.register(container_actions, ContainerCallback.filter())
+ dp.callback_query.register(do_container_action, ActionCallback.filter())
+ dp.callback_query.register(add_favorite_callback, AddFavoriteCallback.filter())
+ dp.callback_query.register(del_favorite_callback, DelFavoriteCallback.filter())
dp.message.register(execute_commands, ExecuteCommandsSteps.EXECUTE)
+ dp.message.register(add_favorite_state, FavoritesCommandsSteps.ADD)
+ dp.message.register(del_favorite_state, FavoritesCommandsSteps.DEL)
try:
await dp.start_polling(bot)