Compare commits

..

3 Commits

Author SHA1 Message Date
b5fcdd2d38 Merge pull request 'dev_d' (#1) from dev_d into master
Some checks failed
continuous-integration/drone Build is failing
Reviewed-on: #1
2025-09-25 17:22:01 +03:00
3ff9fda739 fix drone file 2025-09-25 17:18:27 +03:00
54e17257d0 drone file configure
Some checks failed
continuous-integration/drone Build is failing
2025-09-25 16:49:23 +03:00
13 changed files with 144 additions and 164 deletions

View File

@@ -1,4 +0,0 @@
from .states import OrderForm, SearchForm, OrderingForm
__all__ = ["SearchForm", "OrderForm", "OrderingForm"]

View File

@@ -1,25 +0,0 @@
from aiogram.fsm.state import State, StatesGroup
class SearchForm(StatesGroup):
search_option = State()
data_to_search = State()
sent_messages = State()
search_result = State()
class OrderForm(StatesGroup):
id = State()
worker_id = State()
status_id = State()
counterparty = State()
customer = State()
commencement_work = State()
end_work = State()
description = State()
class OrderingForm(StatesGroup):
tools_list = State()

View File

@@ -1,8 +1,14 @@
import os
import asyncpg
from sqlalchemy.ext.asyncio import async_sessionmaker, create_async_engine, session
import dotenv
# connection = psycopg2.connect(*(os.getenv(key) for key in ["DATABASE", "DB_HOST", "DB_USER", "DB_PASSWORD"]))
# connection.autocommit = True
dotenv.load_dotenv(".env")
DATABASE_URL = (f"postgresql+asyncpg://{os.getenv('DB_USER')}:{os.getenv('DB_PASSWORD')}@"
f"{os.getenv('DB_HOST')}/{os.getenv('DATABASE')}")
print(DATABASE_URL)
engine = create_async_engine(DATABASE_URL, echo=True)
async_session_ = async_sessionmaker(bind=engine, expire_on_commit=False)

View File

@@ -13,11 +13,10 @@ class Base(DeclarativeBase):
class Worker(Base):
"""
id SERIAL PRIMARY KEY,
telegram_id BIGINT UNIQUE NOT NULL,
name VARCHAR NOT NULL,
telegram_id INTEGER UNIQUE NOT NULL,
name VARCHAR NOT NULL,
email VARCHAR(50),
phone_number VARCHAR(20),
job_title job_title default 'Сборщик',
phone_number VARCHAR(20) NOT NULL,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP
"""
@@ -28,7 +27,7 @@ class Worker(Base):
name = Column(String, nullable=False)
email = Column(String, nullable=True)
phone_number = Column(String, default=None)
job_title = Column(job_title, default='Сборщик')
job_title = Column(job_title, default='Сборщик'),
created_at = Column(Date, server_default=func.now())
updated_at = Column(Date, onupdate=func.now())

View File

@@ -1,18 +1,19 @@
import os
from typing import Any
import logging
from aiogram.types import Message, CallbackQuery
from aiogram.filters import BaseFilter
from keyboards.menu_commands import commands
import os
loggger = logging.getLogger(__name__)
class IsAdmin(BaseFilter):
def __init__(self):
self.admins_ids = list(map(int, os.getenv("BOT_ADMINS").split(",")))
self.admins_ids = os.getenv("BOT_ADMINS").split(",")
async def __call__(self, message: Message | CallbackQuery) -> bool:
return message.from_user.id in self.admins_ids
return str(message.from_user.id) in self.admins_ids
class CommandFilter(BaseFilter):
@@ -22,7 +23,3 @@ class CommandFilter(BaseFilter):
async def __call__(self, message: Message) -> bool:
return message.text.startswith(tuple(self.commands.keys()))
class IsRegister(BaseFilter):
async def __call__(self, message: Message | CallbackQuery, **data: dict[str, Any]) -> bool:
return data.get("through_registration") is True

View File

@@ -1,4 +1 @@
from .Filters import IsAdmin
from .Filters import IsRegister
__all__ = ["IsRegister", "IsAdmin"]
from .Filters import IsAdmin

View File

@@ -1,9 +1,11 @@
import time
import re
from loguru import logger
from aiogram import Router, Bot, F
from aiogram.types import Message, CallbackQuery
from aiogram.types import (Message, ChatMemberUpdated, FSInputFile, CallbackQuery, ReplyKeyboardRemove)
from loguru import logger
from handlers.registration import registration_confirm
from filters.Filters import IsAdmin
from filters.Filters import IsAdmin, CommandFilter
from database import async_session_
admin_router = Router()

View File

@@ -1,39 +1,61 @@
import asyncio
import os
import re
from pathlib import Path
import re
from aiogram import Router, Bot, F
from aiogram.filters import Command
from aiogram.filters import CommandStart, Command
from aiogram.types import Message, CallbackQuery, FSInputFile, InputMediaPhoto, InputMediaVideo, ReplyKeyboardRemove
from aiogram.exceptions import AiogramError
from aiogram.fsm.state import State, StatesGroup
from aiogram.fsm.context import FSMContext
from sqlalchemy import select
from sqlalchemy import select, insert
from loguru import logger
from keyboards import create_inline_kb, button_create
from filters import IsAdmin
from keyboards import create_inline_kb, commands, button_create
from database import async_session_, Order, Worker
from FSM import SearchForm, OrderForm
orders_router = Router()
# orders_router.message.filter()
order_operation_base = {"add_order_photo": "Добавить фото",
"get_order_photo": "Получить фото",
"get_order_components": "Получить список комплектующих",
"get_order_documentation": "Получить документацию",
"finish_work_on_order": "❌ Закрыть"
"get_order_documentation": "Получить документацию"
}
order_operation_update = {"add_order_documentation": "Добавить документацию"}
order_main = {"find_orders": "Найти заказ", "create_order": "Создать заказ"}
order_main = {"find_orders": "Найти заказ"}
order_main_update = {"create_order": "Создать заказ"}
find_order_params = {"search_by_name": "Поиск по названию", "search_by_description": "Поиск по описанию ",
"search_by_id": "Поиск по номеру заказа", "search_by_customer": "Поиск по заказчику"}
class SearchForm(StatesGroup):
search_option = State()
data_to_search = State()
search_result = State()
class OrderForm(StatesGroup):
id = State()
worker_id = State()
status_id = State()
counterparty = State()
customer = State()
commencement_work = State()
end_work = State()
description = State()
@orders_router.message(Command(commands="orders"))
async def orders_menu(message: Message):
order_main_upd = order_main_update if await IsAdmin()(message) else {}
await message.answer(text="Доступные действия с заказами:",
reply_markup=create_inline_kb(width=1, **order_main))
await message.delete()
reply_markup=create_inline_kb(width=1, **order_main, **order_main_upd))
@orders_router.callback_query(lambda x: x.data.startswith("create_order"))
@@ -64,6 +86,10 @@ async def get_order_customer(callback: CallbackQuery, state: FSMContext):
await state.set_state(OrderForm.customer)
# @orders_router.callback_query(OrderForm.counterparty)
# async def get_order_customer(message: Message, state: FSMContext):
#
@orders_router.message(OrderForm.customer)
async def order_description(message: Message, state: FSMContext):
await message.answer("Введите описание заказа в виде ключевых слов (АВР, ПСС, НКУ и т.д.) )")
@@ -110,30 +136,33 @@ async def search_by_item(message: Message, state: FSMContext):
selected_orders = result.scalars().all()
if selected_orders:
await message.answer(text="Список найденных заказов: ")
sent_messages = []
for order in selected_orders:
sent_messages.append(await message.answer(text=f"Номер заказа: {order.id}\n"
f"Заказчик: {order.customer}\n"
f"Статус: {order.status_id}\n"
f"Дата отгрузки: {order.end_work}\n"
f"Дата создания: {order.created_at}\n"
f"Описание: {order.description}",
reply_markup=create_inline_kb(width=1, **{
f"show_order_{order.id}": f"Заказ: №{order.id}"})))
await state.update_data(sent_messages=sent_messages)
await state.set_state(SearchForm.search_result)
await message.answer(text="Список найденных заказов: ",
reply_markup=create_inline_kb(width=1, **dict(
(f"show_order_{order.id}", f"Описание: {order.description or "Отсутствует"}"
f"Заказ: {order.id}\n"
f"Заказчик: {order.customer}\n"
) for order in
selected_orders)))
await state.update_data(search_result=selected_orders)
await state.set_state(SearchForm.search_result)
else:
await message.answer(text="Заказов по вашему запросу не найдено")
await state.clear()
@orders_router.callback_query(SearchForm.search_result and (lambda x: "show_order_" in x.data))
async def show_order(callback: CallbackQuery, state: FSMContext, bot: Bot):
@orders_router.callback_query(SearchForm.search_result)
async def show_order(callback: CallbackQuery, state: FSMContext):
order_id = int(re.search(r"(\d+)", callback.data).group())
orders = await state.get_value("search_result")
order = next(filter(lambda item: order_id == item.id, orders), None)
try:
async with async_session_() as local_session:
result = await local_session.execute(select(Order).filter(Order.id == order_id))
order = result.scalars().first()
except Exception as err:
logger.warning(err)
order_operation_upd = order_operation_update if await IsAdmin()(callback) else {}
if order:
await callback.message.answer(text=f"Номер заказа: {order.id}\n"
@@ -143,22 +172,21 @@ async def show_order(callback: CallbackQuery, state: FSMContext, bot: Bot):
f"Дата отгрузки: {order.end_work}\n"
f"Дата создания: {order.created_at}\n"
f"Описание: {order.description}",
reply_markup=create_inline_kb(width=1, **dict(
reply_markup=create_inline_kb(width=2, **dict(
(f"{clbk}_{order.id}", text) for clbk, text in order_operation_base.items()))
)
for message in await state.get_value("sent_messages"):
await bot.delete_message(chat_id=callback.message.chat.id, message_id=message.message_id)
await state.clear()
await callback.message.delete()
await state.clear()
@orders_router.callback_query(lambda x: x.data.startswith("get_order_photo"))
async def send_order_photos(callback: CallbackQuery, bot: Bot):
order_id = callback.data.split("_")[-1]
media_item: Path
media_group = []
order_photos_path = Path(f"/app/photos/{order_id}/")
os.makedirs(order_photos_path, exist_ok=True)
media_path = order_photos_path.iterdir()
os.makedirs(Path(f"./photos/{order_id}/"), exist_ok=True)
media_path = Path(f"./photos/{order_id}/").iterdir()
if not (media_item := next(media_path, None)):
text = f"Фото по заказу \"{order_id}\" отсутствуют "
else:
@@ -166,7 +194,7 @@ async def send_order_photos(callback: CallbackQuery, bot: Bot):
f"")
await bot.send_message(chat_id=callback.from_user.id, text=text)
while media_item or media_group:
if len(media_group) == 10 or not media_item:
if len(media_group) == 10 or (media_group and not media_item):
await bot.send_media_group(chat_id=callback.from_user.id, media=media_group)
media_group.clear()
if media_item:
@@ -177,10 +205,8 @@ async def send_order_photos(callback: CallbackQuery, bot: Bot):
logger.error(f"Ошибка при обработке {media_path}: {err}")
media_item = next(media_path, None)
try:
pass
except:
pass
await asyncio.sleep(600)
await callback.message.delete()
@orders_router.callback_query(lambda x: x.data.startswith("add_order_photo"))
@@ -195,7 +221,7 @@ async def reply_for_photo(callback: CallbackQuery, bot: Bot):
F.reply_to_message)
async def add_order_photo(message: Message, bot: Bot):
order_id = re.search(r"(\d+)", message.reply_to_message.text).group()
order_photos_path = Path(f"/app/photos/{order_id}/")
order_photos_path = f"/app/photos/{order_id}/"
os.makedirs(order_photos_path, exist_ok=True)
item = message.video or message.photo[-1]
@@ -215,8 +241,3 @@ async def add_order_photo(message: Message, bot: Bot):
await bot.delete_message(chat_id=message.chat.id, message_id=message.reply_to_message.message_id)
except:
pass
@orders_router.callback_query(F.data.startswith("finish_work_on_order"))
async def finish_work_on_order(callback: CallbackQuery):
await callback.message.delete()

View File

@@ -1,18 +1,17 @@
import os
from asyncio import Event, wait_for, TimeoutError
from aiogram import Router, Bot
from aiogram.filters import CommandStart
from aiogram.types import Message, User
from sqlalchemy import insert, select
from loguru import logger
from keyboards import create_inline_kb
from database import async_session_, Worker
from filters import IsRegister
registration_router = Router()
registration_router.message.filter(IsRegister() or CommandStart())
registration_confirm: dict[int, Event] = {}
user_info_template = ("Новый пользователь ждет регистрации:\n"
"Имя: {}\n"
@@ -20,29 +19,28 @@ user_info_template = ("Новый пользователь ждет регист
"Юзернейм: @{}\n"
"ID: @msg_{}\n")
admins_ids = list(map(int, os.getenv("BOT_ADMINS").split(",")))
@registration_router.message(CommandStart())
async def start_command(message: Message, bot: Bot):
async def registration_command(message: Message, bot: Bot):
async with async_session_() as session:
result = await session.execute(select(Worker).where(Worker.telegram_id == message.from_user.id))
user = result.scalars().first()
async with session.begin():
result = await session.execute(select(Worker).where(Worker.telegram_id == message.from_user.id))
user = result.scalars().first()
if not user:
user = message.from_user
dict_for_inline = {f'reg_@{user.id}': 'Allow', f'del_@{user.id}': 'Reject'}
user_info = user_info_template.format(user.first_name, user.last_name if user.last_name else 'Не указана',
user.username if user.username else 'Не указан', user.id)
for admin in list(map(int, os.getenv("BOT_ADMINS").split(","))):
for admin in admins_ids:
try:
await bot.send_message(chat_id=admin, text=user_info)
await bot.send_message(chat_id=admin, text='Зарегистрировать пользователя',
reply_markup=create_inline_kb(width=2, **dict_for_inline))
await message.answer("Запрос на регистрацию отправлен администратору, ожидайте подтверждения.")
except Exception:
logger.error(f"{start_command.__name__} failed")
except Exception as err:
pass
reg_confirm = Event()
registration_confirm[user.id] = reg_confirm
try:
@@ -51,7 +49,7 @@ async def start_command(message: Message, bot: Bot):
async with local_session.begin():
local_session.add(Worker(telegram_id=user.id, name=user.first_name))
await message.answer("Регистрация подтверждена, для просмотра доступных действий нажмите кнопку 'MENU'")
await message.answer("Регистрация подтверждена")
except TimeoutError:
await message.answer("Время ожидания истекло.")
@@ -59,8 +57,3 @@ async def start_command(message: Message, bot: Bot):
else:
await message.answer("Работа бота возобновлена")
@registration_router.message()
async def catch_message(message: Message):
await message.answer("Для работы с ботом, требуется регистрация\nНажмите /start для регистрации")

View File

@@ -1,14 +1,12 @@
import os
import asyncio
from dotenv import load_dotenv
load_dotenv(".env")
from aiogram import Dispatcher, Bot
from handlers import *
from keyboards import set_main_menu
from middlewares import SessionMiddleware
from middlewares import AccessCheckMiddleware
load_dotenv(".env")
bot = Bot(token=os.getenv("TOKEN"))
@@ -18,7 +16,7 @@ async def main() -> None:
dp.startup.register(set_main_menu)
dp.include_router(registration_router)
dp.include_router(admin_router)
dp.update.outer_middleware(SessionMiddleware())
dp.update.outer_middleware(AccessCheckMiddleware())
dp.include_router(orders_router)
dp.include_router(components_router)

View File

@@ -1,4 +1,4 @@
from .outer_middlewares import SessionMiddleware
from .outer_middlewares import AccessCheckMiddleware
__all__ = ["SessionMiddleware"]
__all__ = ["AccessCheckMiddleware"]

View File

@@ -1,29 +1,29 @@
import logging
from typing import Any, Awaitable, Callable, Dict
from aiogram import BaseMiddleware, Bot
from aiogram.types import TelegramObject
from sqlalchemy import select
from loguru import logger
from database import async_session_, Worker
from sqlalchemy import select
class SessionMiddleware(BaseMiddleware):
class AccessCheckMiddleware(BaseMiddleware):
sessions_in_memory_db = set()
async def __call__(
self,
handler: Callable[[TelegramObject, Dict[str, Any]], Awaitable[Any]],
event: TelegramObject,
data: Dict[str, Any]
) -> Any:
logger.info("Session check")
event_data = event.message or event.callback_query
user = event_data.from_user.id
if user not in self.sessions_in_memory_db:
async with async_session_() as session:
result = await session.execute(select(Worker).where(Worker.telegram_id == event_data.from_user.id))
user_in_db = result.scalars().first()
if not user_in_db:
data["through_registration"] = True
else:
self.sessions_in_memory_db.add(user)
return await handler(event, data)
async with session.begin():
result = await session.execute(select(Worker).where(Worker.telegram_id == event_data.from_user.id))
user = result.scalars().first()
if user:
self.sessions_in_memory_db.add(event_data.from_user.id)
return await handler(event, data)
return None

View File

@@ -1,15 +1,19 @@
kind: pipeline
type: docker
name: first_run
name: default
clone:
disable: true
git:
url: https://gitea.ronis0505.tech/ronis_0505/telegram-bot-for-manipulate-orders.git
user:
from_secret: GITEA_USER
password:
from_secret: GITEA_TOKEN
steps:
- name: clone
image: alpine/git
- name: install-dependencies
image: python:3.12
commands:
- git clone https://git.ronis-0505.ru/ronis_0505/telegram-bot-for-manipulate-orders.git .
- pip install -r pyproject.toml
- name: build
image: docker:dind
@@ -20,35 +24,27 @@ steps:
- docker build -t myapp:${DRONE_COMMIT_SHA} .
- name: deploy
image: docker
volumes:
- name: dockersock
path: /var/run/docker.sock
- name: env
path: /srv/prod/telegram_bot/
image: appleboy/ssh
environment:
TOKEN:
from_secret: TOKEN
BOT_ADMINS:
from_secret: BOT_ADMINS
DATABASE:
from_secret: DATABASE
DB_HOST:
from_secret: DB_HOST
DB_USER:
from_secret: DB_USER
DB_PASSWORD:
from_secret: DB_PASSWORD
ENV_CONTENT:
from_secret: DOT_ENV_CONTENT
commands:
- docker stop myapp || true
- docker rm myapp || true
- docker run --name=myapp -d --network=prod_net -v /srv/prod/telegram_bot/photos:/app/photos/ -v /srv/prod/telegram_bot/.env:/app/.env:ro myapp:${DRONE_COMMIT_SHA}
- echo "$ENV_CONTENT" > .env
settings:
host: https://gitea.ronis-0505.ru/ronis_0505/telegram-bot-for-manipulate-orders.git
username: ronis_0505
password: 667766
script:
- docker pull myapp:${DRONE_COMMIT_SHA}
- docker stop myapp || true
- docker rm myapp || true
- docker run -d --name myapp \
--network prod_net \
-v /srv/prod/telegram_bot/photos:/app/photos/ \
myapp:${DRONE_COMMIT_SHA}
volumes:
- name: dockersock
host:
path: /var/run/docker.sock
- name: env
host:
path: /srv/prod/telegram_bot/