feat: add admin promotion broadcast system and psql health check

- Add `/push` command for admins to broadcast messages to all users
  with approve/decline confirmation flow (handlers/ads.py)
- Add common handler for undefined commands and delete callback
- Add `UserRepository.get_users()` to fetch all users
- Add `UserService.send_promotion_to_users()` with error logging
- Add promotion confirmation keyboard and FSM state
- Add PostgreSQL availability check in startup.sh before migrations
- Add new text constants for promotion flow
This commit is contained in:
2026-04-15 20:19:07 +07:00
parent 0ac8ed8d64
commit 606aa4b89f
13 changed files with 160 additions and 2 deletions

View File

@@ -1,6 +1,8 @@
from .ads import router as ads_router
from .buy import router as buy_router
from .common import router as common_router
from .menus import router as menus_router
from .referals import router as referal_router
from .support import router as support_router
routers = [referal_router, menus_router, support_router, buy_router]
routers = [referal_router, menus_router, ads_router, support_router, buy_router, common_router]

69
handlers/ads.py Normal file
View File

@@ -0,0 +1,69 @@
from aiogram import F, Router
from aiogram.filters import Command
from aiogram.fsm.context import FSMContext
from aiogram.types import CallbackQuery, Message
from sqlalchemy.ext.asyncio import AsyncSession
from config import settings
from keyboards.admins import promotion_confirmation
from keyboards.client import build_return_menu
from schemas.di import DependenciesDTO
from schemas.promotions import NewPromotionContext
from states.admins import AdminStorage
from texts import (
ADMIN_PROMOTION_CONFIRMATION,
ADMIN_PROMOTION_INIT,
ADMIN_STANDARD_FALLBACK,
SUCCESSFULLY_SENT_PROMOTION,
)
router = Router()
@router.message(
F.chat.id == settings.admin_group_id, F.message_thread_id.is_(None), Command("push")
)
async def push_cmd(msg: Message, state: FSMContext):
await state.clear()
ctx = NewPromotionContext(creator_id=msg.from_user.id)
await msg.delete()
await msg.answer(ADMIN_PROMOTION_INIT, reply_markup=build_return_menu("delete", ""))
await state.set_state(AdminStorage.promotion_msg)
await state.set_data({"ctx": ctx})
@router.message(F.chat.id == settings.admin_group_id, AdminStorage.promotion_msg)
async def send_promotion(msg: Message, state: FSMContext):
data = await state.get_data()
ctx: NewPromotionContext | None = data.get("ctx")
if not ctx:
await msg.answer(ADMIN_STANDARD_FALLBACK)
return
ctx.message = msg
await msg.reply(ADMIN_PROMOTION_CONFIRMATION, reply_markup=promotion_confirmation)
await state.set_state(AdminStorage.promotion_msg)
await state.set_data({"ctx": ctx})
@router.callback_query(AdminStorage.promotion_msg, F.data.in_(["approve", "decline"]))
async def promotion_action(
cb: CallbackQuery, state: FSMContext, session: AsyncSession, deps: DependenciesDTO
):
data = await state.get_data()
await state.clear()
await cb.message.delete()
if cb.data == "decline":
return
ctx: NewPromotionContext | None = data.get("ctx")
if not (ctx and ctx.creator_id and ctx.message):
await cb.message.answer(ADMIN_STANDARD_FALLBACK)
return
sent = await deps.user_service.send_promotion_to_users(session, ctx.message)
await cb.message.answer(SUCCESSFULLY_SENT_PROMOTION.format(user_count=len(sent)))

20
handlers/common.py Normal file
View File

@@ -0,0 +1,20 @@
from aiogram import F, Router
from aiogram.fsm.context import FSMContext
from aiogram.types import CallbackQuery, Message
from config import settings
from texts import UNDEFINED_MESSAGE
router = Router()
@router.message(F.chat.id != settings.admin_group_id)
async def undefined_cmd(msg: Message):
await msg.delete()
await msg.answer(UNDEFINED_MESSAGE)
@router.callback_query(F.data == "delete")
async def delete_cb(cb: CallbackQuery, state: FSMContext):
await state.clear()
await cb.message.delete()

View File

@@ -53,7 +53,6 @@ async def received_message(
F.from_user.is_bot == False, # noqa: E712
~Command("close"),
~Command("refpay"),
F.text,
)
async def handle_admin_message(
msg: Message, state: FSMContext, session: AsyncSession, deps: DependenciesDTO