240 lines
9.9 KiB
Python
240 lines
9.9 KiB
Python
"""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,
|
|
) |