Третий стрим
This commit is contained in:
114
alembic.ini
Normal file
114
alembic.ini
Normal file
@ -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
|
1
alembic/README
Normal file
1
alembic/README
Normal file
@ -0,0 +1 @@
|
|||||||
|
Generic single-database configuration with an async dbapi.
|
94
alembic/env.py
Normal file
94
alembic/env.py
Normal file
@ -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()
|
26
alembic/script.py.mako
Normal file
26
alembic/script.py.mako
Normal file
@ -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"}
|
0
app/callbacks/__init__.py
Normal file
0
app/callbacks/__init__.py
Normal file
10
app/callbacks/callback_docker.py
Normal file
10
app/callbacks/callback_docker.py
Normal file
@ -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
|
13
app/callbacks/callback_favorites.py
Normal file
13
app/callbacks/callback_favorites.py
Normal file
@ -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
|
@ -2,47 +2,42 @@ import subprocess
|
|||||||
|
|
||||||
from aiogram.types import Message, CallbackQuery
|
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.keyboards.docker_keyboards import container_names_keyboard, container_actions_keyboard
|
||||||
|
from app.utils.text_splitter import split_text
|
||||||
|
|
||||||
|
|
||||||
async def get_containers(message: Message):
|
async def get_containers(message: Message):
|
||||||
sub = subprocess.check_output("docker ps -a").decode()
|
sub = subprocess.check_output("docker ps -a", shell=True).decode()
|
||||||
keyboard = container_names_keyboard(sub)
|
messages = split_text(sub)
|
||||||
|
|
||||||
|
for m in messages:
|
||||||
await message.answer(
|
await message.answer(
|
||||||
text=f"<pre><code>{sub}</code></pre>",
|
text=f"<pre><code>{m}</code></pre>",
|
||||||
parse_mode="HTML",
|
parse_mode="HTML",
|
||||||
reply_markup=keyboard.as_markup()
|
|
||||||
)
|
)
|
||||||
|
|
||||||
|
keyboard = container_names_keyboard(sub)
|
||||||
|
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)
|
keyboard = container_actions_keyboard(name)
|
||||||
await call.message.answer(
|
await callback.message.answer(
|
||||||
text=f"Выберите действие для контейнера {name}",
|
text=f"Выберите действие для контейнера {name}",
|
||||||
parse_mode="HTML",
|
parse_mode="HTML",
|
||||||
reply_markup=keyboard.as_markup()
|
reply_markup=keyboard.as_markup()
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
async def do_container_action(call: CallbackQuery):
|
async def do_container_action(callback: CallbackQuery, callback_data: ActionCallback):
|
||||||
_, action, name = call.data.split("_")
|
action, name = callback_data.name, callback_data.action
|
||||||
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"Произошла необъяснимая ошибка"
|
|
||||||
|
|
||||||
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,
|
text=message,
|
||||||
)
|
)
|
||||||
|
@ -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.utils.commands import set_commands
|
||||||
from app import views
|
from app import views
|
||||||
|
|
||||||
|
|
||||||
async def start_bot():
|
async def start_bot():
|
||||||
await set_commands(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():
|
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())
|
||||||
|
@ -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()
|
||||||
|
@ -6,6 +6,7 @@ from aiogram.types import Message
|
|||||||
|
|
||||||
from app import views
|
from app import views
|
||||||
from app.utils.statesform import ExecuteCommandsSteps
|
from app.utils.statesform import ExecuteCommandsSteps
|
||||||
|
from app.utils.text_splitter import split_text
|
||||||
|
|
||||||
|
|
||||||
async def multiply_commands(message: Message, state: FSMContext):
|
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:
|
except subprocess.CalledProcessError as e:
|
||||||
res = views.subprocess_error(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)
|
await state.set_state(ExecuteCommandsSteps.EXECUTE)
|
||||||
|
@ -3,10 +3,15 @@ import subprocess
|
|||||||
from aiogram.types import Message
|
from aiogram.types import Message
|
||||||
|
|
||||||
from app import views
|
from app import views
|
||||||
|
from app.utils.text_splitter import split_text
|
||||||
|
|
||||||
|
|
||||||
async def execute_command(message: Message):
|
async def execute_command(message: Message):
|
||||||
user_command = message.text.split()[1:]
|
user_command = message.text.split()[1:]
|
||||||
sub = subprocess.check_output(user_command, shell=True)
|
sub = subprocess.check_output(user_command, shell=True)
|
||||||
res = sub.decode().replace("&", "&").replace("<", "<").replace(">", ">")
|
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")
|
||||||
|
@ -1,16 +1,17 @@
|
|||||||
from aiogram.types import InlineKeyboardButton
|
from aiogram.types import InlineKeyboardButton
|
||||||
from aiogram.utils.keyboard import InlineKeyboardBuilder
|
from aiogram.utils.keyboard import InlineKeyboardBuilder
|
||||||
|
|
||||||
|
from app.callbacks.callback_docker import ContainerCallback, ActionCallback
|
||||||
|
|
||||||
|
|
||||||
def container_names_keyboard(stdout: str):
|
def container_names_keyboard(stdout: str):
|
||||||
container_names = [line.split(" ")[-1].strip() for line in stdout.splitlines()[1:]]
|
container_names = [line.split(" ")[-1].strip() for line in stdout.splitlines()[1:]]
|
||||||
builder = InlineKeyboardBuilder()
|
builder = InlineKeyboardBuilder()
|
||||||
for name in container_names:
|
for name in container_names:
|
||||||
data = f"container_{name}"
|
|
||||||
builder.add(
|
builder.add(
|
||||||
InlineKeyboardButton(
|
InlineKeyboardButton(
|
||||||
text=name,
|
text=name,
|
||||||
callback_data=data,
|
callback_data=ContainerCallback(name=name).pack(),
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
builder.adjust(1)
|
builder.adjust(1)
|
||||||
@ -23,19 +24,19 @@ def container_actions_keyboard(name: str):
|
|||||||
|
|
||||||
builder.add(InlineKeyboardButton(
|
builder.add(InlineKeyboardButton(
|
||||||
text="Запустить контейнер",
|
text="Запустить контейнер",
|
||||||
callback_data=f"action_start_{name}",
|
callback_data=ActionCallback(name=name, action="start").pack(),
|
||||||
))
|
))
|
||||||
builder.add(InlineKeyboardButton(
|
builder.add(InlineKeyboardButton(
|
||||||
text="Остановить контейнер",
|
text="Остановить контейнер",
|
||||||
callback_data=f"action_stop_{name}",
|
callback_data=ActionCallback(name=name, action="stop").pack(),
|
||||||
))
|
))
|
||||||
builder.add(InlineKeyboardButton(
|
builder.add(InlineKeyboardButton(
|
||||||
text="Перезапустить контейнер",
|
text="Перезапустить контейнер",
|
||||||
callback_data=f"action_restart_{name}",
|
callback_data=ActionCallback(name=name, action="restart").pack(),
|
||||||
))
|
))
|
||||||
builder.add(InlineKeyboardButton(
|
builder.add(InlineKeyboardButton(
|
||||||
text="Удалить контейнер",
|
text="Удалить контейнер",
|
||||||
callback_data=f"action_delete_{name}",
|
callback_data=ActionCallback(name=name, action="delete").pack(),
|
||||||
))
|
))
|
||||||
builder.adjust(1)
|
builder.adjust(1)
|
||||||
|
|
||||||
|
52
app/keyboards/favorites_keyboards.py
Normal file
52
app/keyboards/favorites_keyboards.py
Normal file
@ -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()
|
0
app/middlewares/__init__.py
Normal file
0
app/middlewares/__init__.py
Normal file
18
app/middlewares/admin_middleware.py
Normal file
18
app/middlewares/admin_middleware.py
Normal file
@ -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)
|
9
app/models/__init__.py
Normal file
9
app/models/__init__.py
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
from .base import Base
|
||||||
|
from .user import User
|
||||||
|
from .favorites import Favorites
|
||||||
|
|
||||||
|
__all__ = [
|
||||||
|
"Base",
|
||||||
|
"User",
|
||||||
|
"Favorites",
|
||||||
|
]
|
7
app/models/base.py
Normal file
7
app/models/base.py
Normal file
@ -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)
|
17
app/models/favorites.py
Normal file
17
app/models/favorites.py
Normal file
@ -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")
|
18
app/models/user.py
Normal file
18
app/models/user.py
Normal file
@ -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")
|
0
app/schemas/__init__.py
Normal file
0
app/schemas/__init__.py
Normal file
10
app/schemas/favorites_schema.py
Normal file
10
app/schemas/favorites_schema.py
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
from pydantic import BaseModel
|
||||||
|
|
||||||
|
|
||||||
|
class FavoritesSchemaInput(BaseModel):
|
||||||
|
command: str
|
||||||
|
user_id: int
|
||||||
|
|
||||||
|
|
||||||
|
class FavoritesSchemaOutput(FavoritesSchemaInput):
|
||||||
|
id: int
|
12
app/schemas/user_schema.py
Normal file
12
app/schemas/user_schema.py
Normal file
@ -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
|
@ -1,16 +1,25 @@
|
|||||||
import os
|
import os
|
||||||
from dataclasses import dataclass
|
from typing import Union
|
||||||
|
|
||||||
from aiogram import Bot
|
from aiogram import Bot
|
||||||
from dotenv import load_dotenv
|
from pydantic import SecretStr
|
||||||
|
from pydantic_settings import BaseSettings
|
||||||
load_dotenv()
|
from sqlalchemy.ext.asyncio import create_async_engine, async_sessionmaker
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
class Secrets(BaseSettings):
|
||||||
class Secrets:
|
token: SecretStr
|
||||||
token: str = os.environ.get("token")
|
admin_id: Union[SecretStr.get_secret_value, int]
|
||||||
admin_id: int = os.environ.get("admin_id")
|
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())
|
||||||
|
50
app/utils/db_actions.py
Normal file
50
app/utils/db_actions.py
Normal file
@ -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()
|
@ -3,3 +3,8 @@ from aiogram.fsm.state import StatesGroup, State
|
|||||||
|
|
||||||
class ExecuteCommandsSteps(StatesGroup):
|
class ExecuteCommandsSteps(StatesGroup):
|
||||||
EXECUTE = State()
|
EXECUTE = State()
|
||||||
|
|
||||||
|
|
||||||
|
class FavoritesCommandsSteps(StatesGroup):
|
||||||
|
ADD = State()
|
||||||
|
DEL = State()
|
20
app/utils/text_splitter.py
Normal file
20
app/utils/text_splitter.py
Normal file
@ -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
|
@ -5,6 +5,12 @@ menu = {
|
|||||||
"docker_list": "Список Docker-контейнеров",
|
"docker_list": "Список Docker-контейнеров",
|
||||||
}
|
}
|
||||||
|
|
||||||
|
actions = {
|
||||||
|
"start": "Контейнер {} успешно запущен",
|
||||||
|
"stop": "Контейнер {} успешно остановлен",
|
||||||
|
"restart": "Контейнер {} успешно перезапущен",
|
||||||
|
"delete": "Контейнер {} успешно удалён",
|
||||||
|
}
|
||||||
|
|
||||||
def start_bot_message():
|
def start_bot_message():
|
||||||
return "Бот запущен"
|
return "Бот запущен"
|
||||||
|
18
main.py
18
main.py
@ -3,18 +3,25 @@ import asyncio
|
|||||||
from aiogram import Dispatcher, F
|
from aiogram import Dispatcher, F
|
||||||
from aiogram.filters import Command
|
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.docker_commands import get_containers, container_actions, do_container_action
|
||||||
from app.handlers.events import start_bot, stop_bot
|
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.multiply_commands import multiply_commands, execute_commands
|
||||||
from app.handlers.simple import start_command
|
from app.handlers.simple import start_command
|
||||||
from app.handlers.single_command import execute_command
|
from app.handlers.single_command import execute_command
|
||||||
|
from app.middlewares.admin_middleware import AdminMiddleware
|
||||||
from app.settings import bot
|
from app.settings import bot
|
||||||
from app.utils.statesform import ExecuteCommandsSteps
|
from app.utils.statesform import ExecuteCommandsSteps, FavoritesCommandsSteps
|
||||||
|
|
||||||
|
|
||||||
async def start():
|
async def start():
|
||||||
dp = Dispatcher()
|
dp = Dispatcher()
|
||||||
|
|
||||||
|
dp.update.middleware(AdminMiddleware())
|
||||||
|
|
||||||
dp.startup.register(start_bot)
|
dp.startup.register(start_bot)
|
||||||
dp.shutdown.register(stop_bot)
|
dp.shutdown.register(stop_bot)
|
||||||
|
|
||||||
@ -22,11 +29,16 @@ async def start():
|
|||||||
dp.message.register(execute_command, Command(commands="command"))
|
dp.message.register(execute_command, Command(commands="command"))
|
||||||
dp.message.register(multiply_commands, Command(commands="multiply_commands"))
|
dp.message.register(multiply_commands, Command(commands="multiply_commands"))
|
||||||
dp.message.register(get_containers, Command(commands="docker_list"))
|
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(container_actions, ContainerCallback.filter())
|
||||||
dp.callback_query.register(do_container_action, F.data.startswith("action"))
|
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(execute_commands, ExecuteCommandsSteps.EXECUTE)
|
||||||
|
dp.message.register(add_favorite_state, FavoritesCommandsSteps.ADD)
|
||||||
|
dp.message.register(del_favorite_state, FavoritesCommandsSteps.DEL)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
await dp.start_polling(bot)
|
await dp.start_polling(bot)
|
||||||
|
Reference in New Issue
Block a user