Initial commit
This commit is contained in:
240
routers/private.py
Normal file
240
routers/private.py
Normal file
@@ -0,0 +1,240 @@
|
||||
"""Handlers for private (DM) interactions: TOS, menu, upload FSM, chat."""
|
||||
import logging
|
||||
import time
|
||||
|
||||
from aiogram import F, Router, types
|
||||
from aiogram.filters import Command, CommandStart
|
||||
from aiogram.fsm.context import FSMContext
|
||||
|
||||
import bot_state as state_store
|
||||
from config import (
|
||||
CHAT_CONTACT_ADMIN_GROUP_ID,
|
||||
DAILY_SUBMISSION_LIMIT,
|
||||
REVIEW_ADMIN_CHATROOM_SEMIPUBLIC_GROUP_ID,
|
||||
INVITELINK_ARCH,
|
||||
INVITELINK_CHAT,
|
||||
BOT_USERNAME,
|
||||
)
|
||||
from keyboards import admin_kb, anonymous_choice_kb, confirm_kb, menu_kb
|
||||
from persistence import save_confirmed_users
|
||||
from states import ChatSetup, Upload
|
||||
from utils import (
|
||||
build_message_preview,
|
||||
build_quote,
|
||||
cancel_upload_prompt,
|
||||
get_admin_display_name,
|
||||
schedule_upload_prompt,
|
||||
send_media_items,
|
||||
start_chat_session,
|
||||
)
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
router = Router(name="private")
|
||||
|
||||
# ── TOS confirm ───────────────────────────────────────────────────────────────
|
||||
|
||||
@router.callback_query(F.data == "tos_confirm")
|
||||
async def tos_confirm_cb(cb: types.CallbackQuery):
|
||||
state_store.confirmed_users.add(cb.from_user.id)
|
||||
save_confirmed_users()
|
||||
try:
|
||||
await cb.message.edit_reply_markup(reply_markup=None)
|
||||
except Exception:
|
||||
pass
|
||||
await cb.message.answer("Choose:", reply_markup=menu_kb())
|
||||
await cb.answer()
|
||||
|
||||
# ── /start ────────────────────────────────────────────────────────────────────
|
||||
|
||||
@router.message(CommandStart())
|
||||
async def cmd_start(message: types.Message, state: FSMContext):
|
||||
await message.answer(
|
||||
"The other videos/images can be found in the channel.\n"
|
||||
"Channel invite links:\n"
|
||||
f"[Archive]({INVITELINK_ARCH}) | [Chat]({INVITELINK_CHAT})",
|
||||
parse_mode="Markdown",
|
||||
)
|
||||
await message.answer("Choose:", reply_markup=menu_kb())
|
||||
|
||||
# ── Menu buttons ──────────────────────────────────────────────────────────────
|
||||
|
||||
@router.callback_query(F.data == "menu_upload")
|
||||
async def menu_upload_cb(cb: types.CallbackQuery, state: FSMContext):
|
||||
if cb.from_user.id in state_store.chat_sessions:
|
||||
await cb.answer(
|
||||
"Your chat is active. Send /stop to return to the submission menu.",
|
||||
show_alert=True,
|
||||
)
|
||||
return
|
||||
cancel_upload_prompt(cb.from_user.id)
|
||||
await state.set_state(Upload.waiting_media)
|
||||
await state.update_data(media=[])
|
||||
await cb.message.answer("Send photos or videos. You can send multiple, then press Submit.")
|
||||
await cb.answer()
|
||||
|
||||
|
||||
@router.callback_query(F.data == "menu_chat")
|
||||
async def menu_chat_cb(cb: types.CallbackQuery, state: FSMContext):
|
||||
if cb.from_user.id in state_store.banned_chat_users:
|
||||
await cb.answer("You are not allowed to contact the administrators.", show_alert=True)
|
||||
return
|
||||
if cb.from_user.id in state_store.chat_sessions:
|
||||
await cb.answer(
|
||||
"Your chat is already active. Send /stop to close it.", show_alert=True
|
||||
)
|
||||
return
|
||||
cancel_upload_prompt(cb.from_user.id)
|
||||
await state.clear()
|
||||
await state.set_state(ChatSetup.waiting_anonymous_choice)
|
||||
await cb.message.answer("Do you want to remain anonymous?", reply_markup=anonymous_choice_kb)
|
||||
await cb.answer()
|
||||
|
||||
# ── Anonymous choice ──────────────────────────────────────────────────────────
|
||||
|
||||
@router.callback_query(F.data == "menu_anon_yes")
|
||||
async def menu_anon_yes_cb(cb: types.CallbackQuery, state: FSMContext):
|
||||
await state.set_state(ChatSetup.waiting_anonymous_name)
|
||||
await cb.message.answer("What name do you want to use?")
|
||||
await cb.answer()
|
||||
|
||||
|
||||
@router.callback_query(F.data == "menu_anon_no")
|
||||
async def menu_anon_no_cb(cb: types.CallbackQuery, state: FSMContext):
|
||||
await start_chat_session(cb.bot, cb.from_user, cb.message.answer, state)
|
||||
await cb.answer()
|
||||
|
||||
|
||||
@router.message(ChatSetup.waiting_anonymous_name, F.text)
|
||||
async def anonymous_name(message: types.Message, state: FSMContext):
|
||||
name = message.text.strip()
|
||||
if not name:
|
||||
await message.answer("Please send a valid name.")
|
||||
return
|
||||
await start_chat_session(message.bot, message.from_user, message.answer, state, anonymous_name=name)
|
||||
|
||||
# ── /stop ─────────────────────────────────────────────────────────────────────
|
||||
|
||||
@router.message(Command("stop"), F.chat.type == "private")
|
||||
async def cmd_stop(message: types.Message, state: FSMContext):
|
||||
cancel_upload_prompt(message.from_user.id)
|
||||
state_store.upload_prompt_msg_ids.pop(message.from_user.id, None)
|
||||
await state.clear()
|
||||
if state_store.chat_sessions.pop(message.from_user.id, None):
|
||||
await message.answer("Chat stopped.", reply_markup=menu_kb())
|
||||
else:
|
||||
await message.answer("Choose:", reply_markup=menu_kb())
|
||||
|
||||
# ── Media upload FSM ──────────────────────────────────────────────────────────
|
||||
|
||||
@router.message(Upload.waiting_media, F.photo | F.video)
|
||||
async def handle_media(message: types.Message, state: FSMContext):
|
||||
if message.photo:
|
||||
file_id, file_unique_id, file_type = (
|
||||
message.photo[-1].file_id,
|
||||
message.photo[-1].file_unique_id,
|
||||
"photo",
|
||||
)
|
||||
else:
|
||||
file_id, file_unique_id, file_type = (
|
||||
message.video.file_id,
|
||||
message.video.file_unique_id,
|
||||
"video",
|
||||
)
|
||||
data = await state.get_data()
|
||||
media = data.get("media", [])
|
||||
media.append({"file_id": file_id, "file_unique_id": file_unique_id, "type": file_type})
|
||||
await state.update_data(media=media)
|
||||
await schedule_upload_prompt(message.bot, message.chat.id, message.from_user.id)
|
||||
|
||||
|
||||
@router.callback_query(F.data == "submit")
|
||||
async def cb_submit(cb: types.CallbackQuery, state: FSMContext):
|
||||
if cb.from_user.id in state_store.submitting_users:
|
||||
await cb.answer()
|
||||
return
|
||||
state_store.submitting_users.add(cb.from_user.id)
|
||||
try:
|
||||
cancel_upload_prompt(cb.from_user.id)
|
||||
state_store.upload_prompt_msg_ids.pop(cb.from_user.id, None)
|
||||
|
||||
data = await state.get_data()
|
||||
media = list(data.get("media", []))
|
||||
if not media:
|
||||
await cb.answer("Send at least one photo or video first.", show_alert=True)
|
||||
return
|
||||
|
||||
user = cb.from_user
|
||||
current_day = int(time.time() // 86400)
|
||||
user_limit = state_store.daily_submissions.get(user.id)
|
||||
if not user_limit or user_limit["day"] != current_day:
|
||||
user_limit = {"day": current_day, "count": 0}
|
||||
state_store.daily_submissions[user.id] = user_limit
|
||||
if user_limit["count"] >= DAILY_SUBMISSION_LIMIT:
|
||||
await cb.message.answer(
|
||||
f"You reached your daily limit of {DAILY_SUBMISSION_LIMIT} submissions. "
|
||||
"Please try again tomorrow."
|
||||
)
|
||||
await cb.answer()
|
||||
return
|
||||
|
||||
state_store.counter += 1
|
||||
sub_id = state_store.counter
|
||||
state_store.submissions[sub_id] = {"user_id": user.id, "media": media}
|
||||
caption = "New submission from @" + (user.username or user.full_name)
|
||||
try:
|
||||
await send_media_items(cb.bot, REVIEW_ADMIN_CHATROOM_SEMIPUBLIC_GROUP_ID, media)
|
||||
await cb.bot.send_message(
|
||||
REVIEW_ADMIN_CHATROOM_SEMIPUBLIC_GROUP_ID,
|
||||
caption,
|
||||
reply_markup=admin_kb(sub_id),
|
||||
)
|
||||
except Exception:
|
||||
logger.exception("Failed to forward submission %s to review group", sub_id)
|
||||
|
||||
user_limit["count"] += 1
|
||||
await state.clear()
|
||||
await cb.message.answer("Submitted ✅", reply_markup=menu_kb())
|
||||
await cb.answer()
|
||||
finally:
|
||||
state_store.submitting_users.discard(cb.from_user.id)
|
||||
|
||||
|
||||
@router.callback_query(F.data == "cancel")
|
||||
async def cb_cancel(cb: types.CallbackQuery, state: FSMContext):
|
||||
cancel_upload_prompt(cb.from_user.id)
|
||||
state_store.upload_prompt_msg_ids.pop(cb.from_user.id, None)
|
||||
await state.clear()
|
||||
await cb.message.answer("Cancelled.", reply_markup=menu_kb())
|
||||
await cb.answer()
|
||||
|
||||
# ── Active chat (DM → admin group) ───────────────────────────────────────────
|
||||
|
||||
@router.message(F.chat.type == "private")
|
||||
async def active_chat_router(message: types.Message, state: FSMContext):
|
||||
session = state_store.chat_sessions.get(message.from_user.id)
|
||||
if not session:
|
||||
return
|
||||
preview = build_message_preview(message)
|
||||
header = f"From: {session['display_name']}\n\n{preview}"
|
||||
try:
|
||||
if message.text:
|
||||
sent = await message.bot.send_message(CHAT_CONTACT_ADMIN_GROUP_ID, header)
|
||||
state_store.chat_message_map[sent.message_id] = {
|
||||
"user_id": message.from_user.id,
|
||||
"quote": preview,
|
||||
}
|
||||
else:
|
||||
info_msg = await message.bot.send_message(CHAT_CONTACT_ADMIN_GROUP_ID, header)
|
||||
copied_msg = await message.bot.copy_message(
|
||||
CHAT_CONTACT_ADMIN_GROUP_ID, message.chat.id, message.message_id
|
||||
)
|
||||
for mid in (info_msg.message_id, copied_msg.message_id):
|
||||
state_store.chat_message_map[mid] = {
|
||||
"user_id": message.from_user.id,
|
||||
"quote": preview,
|
||||
}
|
||||
except Exception:
|
||||
logger.exception(
|
||||
"Failed to forward chat message from user %s to admin group",
|
||||
message.from_user.id,
|
||||
)
|
||||
Reference in New Issue
Block a user