Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions src/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,3 +11,5 @@

settings = Settings()
logger = logging.getLogger("saintbot")

RATE_LIMIT_DELAY = 0.05
5 changes: 4 additions & 1 deletion src/db/db.py
Original file line number Diff line number Diff line change
Expand Up @@ -437,8 +437,11 @@ async def get_stats():
started_rooms = started_rooms_raw[0] if started_rooms_raw else 0

return total_users, participants, rooms_total, started_rooms
return True

def get_all_users():
raw_users = cur.execute("SELECT DISTINCT tg_id FROM users").fetchall()
for user_id in raw_users:
yield user_id[0]

if __name__ == "__main__":
asyncio.run(start_db())
7 changes: 7 additions & 0 deletions src/handlers/admin_command.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@

from src.config import settings, logger
from src.db import db
from src.utilities.notification import broadcast

router = Router(name=__name__)

Expand Down Expand Up @@ -60,3 +61,9 @@ async def status(msg: Message):
f"Комнат всего: {rooms_total}\n"
f"Комнат с запущенным ивентом: {started_rooms}"
)

@router.message(Command("test"))
async def test(msg: Message):
if not _is_admin(msg):
return
await broadcast(msg.bot,db.get_all_users(),"test",delay=0.1)
107 changes: 59 additions & 48 deletions src/handlers/room_admin.py
Original file line number Diff line number Diff line change
@@ -1,25 +1,18 @@
import asyncio
import base64

from aiogram import F, Router
from aiogram.exceptions import TelegramRetryAfter
from aiogram.types import Message, CallbackQuery
from aiogram.filters import Command
from aiogram.fsm.context import FSMContext
from aiogram.types import CallbackQuery

from src.config import logger
from src.config import RATE_LIMIT_DELAY
from src.db import db
from src.handlers.common import EFFECT_IDS
from src.keyboards import common_kb, room_admin_kb
from src.states.states import CallbackFactory, RemoveCallbackFactory
from src.texts import messages, text
from src.texts import messages
from src.texts.callback_actions import CallbackAction
from src.utilities import notification
from src.utilities import utils


RATE_LIMIT_DELAY = 0.05


async def get_room_name(room_iden):
return f"{room_iden[:-4]}:{room_iden[-4:]}"

Expand All @@ -29,7 +22,7 @@ async def get_room_name(room_iden):

@router.callback_query(CallbackFactory.filter(F.action == CallbackAction.DELETE_ROOM))
async def delete_room(
call: CallbackQuery, callback_data: CallbackFactory, state: FSMContext
call: CallbackQuery, callback_data: CallbackFactory, state: FSMContext
):
isMemberOrAdmin = await db.check_room_and_member(
call.from_user.id, callback_data.room_iden
Expand All @@ -53,7 +46,7 @@ async def delete_room(
CallbackFactory.filter(F.action == CallbackAction.CONFIRM_DELETE)
)
async def delete_room(
call: CallbackQuery, callback_data: CallbackFactory, state: FSMContext
call: CallbackQuery, callback_data: CallbackFactory, state: FSMContext
):
isMemberOrAdmin = await db.check_room_and_member(
call.from_user.id, callback_data.room_iden
Expand All @@ -75,7 +68,7 @@ async def delete_room(

@router.callback_query(CallbackFactory.filter(F.action == CallbackAction.REMOVE_MEMBER))
async def remove_member(
call: CallbackQuery, callback_data: CallbackFactory, state: FSMContext
call: CallbackQuery, callback_data: CallbackFactory, state: FSMContext
):
members, *_ = await db.get_members_list(callback_data.room_iden)

Expand All @@ -87,7 +80,7 @@ async def remove_member(
RemoveCallbackFactory.filter(F.action == CallbackAction.REMOVE_MEMBER)
)
async def removing_member(
call: CallbackQuery, callback_data: CallbackFactory, state: FSMContext
call: CallbackQuery, callback_data: CallbackFactory, state: FSMContext
):
isMemberOrAdmin = await db.check_room_and_member(
callback_data.user_id, callback_data.room_iden
Expand Down Expand Up @@ -117,7 +110,7 @@ async def removing_member(

@router.callback_query(CallbackFactory.filter(F.action == CallbackAction.START_EVENT))
async def start_event(
call: CallbackQuery, callback_data: CallbackFactory, state: FSMContext
call: CallbackQuery, callback_data: CallbackFactory, state: FSMContext
):
isMemberOrAdmin = await db.check_room_and_member(
call.from_user.id, callback_data.room_iden
Expand Down Expand Up @@ -158,36 +151,54 @@ async def start_event(
messages.event_started(room_name),
reply_markup=await room_admin_kb.room_admin_kb(callback_data.room_iden),
)
await notification.broadcast(call.bot, members,
text=messages.event_started_notify(room_name),
reply_markup=await common_kb.ok_kb("None", asAdmin=False),
delay=RATE_LIMIT_DELAY,
message_effect_id=EFFECT_IDS["🎉"]
)
await call.answer("Уведомление о начале события отправлено")


@router.callback_query(CallbackFactory.filter(F.action == CallbackAction.REMIND_ABOUT_EVENT))
async def remind_about_event(
call: CallbackQuery, callback_data: CallbackFactory, state: FSMContext
):
isMemberOrAdmin = await db.check_room_and_member(
call.from_user.id, callback_data.room_iden
)
room_name = await get_room_name(callback_data.room_iden)

if isMemberOrAdmin == "ROOM NOT EXISTS":
await call.message.edit_text(
messages.room_not_exists(room_name),
reply_markup=await common_kb.ok_kb("None", asAdmin=False),
)
return

if isMemberOrAdmin == "MEMBER NOT EXISTS":
await call.message.edit_text(
messages.not_a_member(room_name),
reply_markup=await common_kb.ok_kb("None", asAdmin=False),
)
return

status = await db.isStarted(callback_data.room_iden)
if not status:
await call.message.edit_text(
messages.event_not_started(room_name),
reply_markup=await room_admin_kb.room_admin_kb(callback_data.room_iden),
)
return

members, admin, isAdminMember = await db.get_members_list(callback_data.room_iden)
if isAdminMember:
members.append(admin)

for user_id in members:
try:
await call.bot.send_message(
chat_id=user_id,
text=messages.event_started_notify(room_name),
reply_markup=await common_kb.ok_kb("None", asAdmin=False),
message_effect_id=EFFECT_IDS["🎉"],
)
except TelegramRetryAfter as e:
await asyncio.sleep(e.retry_after)
try:
await call.bot.send_message(
chat_id=user_id,
text=messages.event_started_notify(room_name),
reply_markup=await common_kb.ok_kb("None", asAdmin=False),
message_effect_id=EFFECT_IDS["🎉"],
)
except Exception as retry_error:
logger.warning(
"Failed to notify user %s in room %s after retry: %s",
user_id,
callback_data.room_iden,
retry_error,
)
except Exception as e:
logger.warning(
"Failed to notify user %s in room %s: %s",
user_id,
callback_data.room_iden,
e,
)
await asyncio.sleep(RATE_LIMIT_DELAY)
members = [member[0] for member in members]
await notification.broadcast(call.bot, members,
text=messages.remind_notify(room_name),
reply_markup=await common_kb.ok_kb("None", asAdmin=False),
delay=RATE_LIMIT_DELAY,
)
await call.answer("Напоминание отправлено")
10 changes: 10 additions & 0 deletions src/keyboards/room_admin_kb.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,16 @@ async def room_admin_kb(room_iden):
).pack(),
)
],
[
InlineKeyboardButton(
text="🔔Напомнить всем о мероприятии",
callback_data=states.CallbackFactory(
action=CallbackAction.REMIND_ABOUT_EVENT,
room_iden=room_iden,
asAdmin=True,
).pack(),
)
],
[
InlineKeyboardButton(
text="✏️Изменить настройки комнаты",
Expand Down
2 changes: 2 additions & 0 deletions src/texts/callback_actions.py
Original file line number Diff line number Diff line change
Expand Up @@ -34,3 +34,5 @@ class CallbackAction:
SET_ROOM_TYPE_CENTRAL = "set_room_type_central"
SET_ROOM_TYPE_THROW = "set_room_type_throw"
SHOW_ROOM_SETTINGS = "show_room_settings"

REMIND_ABOUT_EVENT = "remind_about_event"
7 changes: 7 additions & 0 deletions src/texts/messages.py
Original file line number Diff line number Diff line change
Expand Up @@ -141,6 +141,13 @@ def event_started(room_name: str) -> str:
def event_started_notify(room_name: str) -> str:
return f"Событие в комнате {room_name} началось\nПроверте кому вы дарите"

def remind_notify(room_name: str) -> str:
return (
f"🔔 Напоминание: событие в комнате {room_name} уже идёт.\n"
"Зайдите в комнату и нажмите «🎁Кому я дарю», чтобы проверить получателя, "
"и подготовьте подарок."
)


def invitation_text(room_name: str) -> str:
return (
Expand Down
43 changes: 43 additions & 0 deletions src/utilities/notification.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
import asyncio
from functools import wraps
from typing import AsyncGenerator

from aiogram.exceptions import TelegramRetryAfter

from src.config import logger


def safe_send(max_retries: int = 1):
def deco(fn):
@wraps(fn)
async def wrapper(*args, **kwargs):
attempt = 0
while True:
try:
return await fn(*args, **kwargs)
except TelegramRetryAfter as e:
attempt += 1
if attempt > max_retries:
logger.warning("RetryAfter maxed: %s", e)
return None
await asyncio.sleep(e.retry_after)
except Exception as e:
logger.warning("Send failed: %s", e)
return None

return wrapper

return deco

@safe_send(max_retries=1)
async def notify_user(bot, user_id: int, text: str, reply_markup=None, message_effect_id=None):
return await bot.send_message(
chat_id=user_id,
text=text,
reply_markup=reply_markup,
message_effect_id=message_effect_id,
)
async def broadcast(bot, user_ids: list[int], text: str, reply_markup=None, message_effect_id=None, delay: float = 0.05):
for uid in user_ids:
await notify_user(bot, uid, text, reply_markup=reply_markup, message_effect_id=message_effect_id)
await asyncio.sleep(delay)