Initial commit
This commit is contained in:
1
routers/__init__.py
Normal file
1
routers/__init__.py
Normal file
@@ -0,0 +1 @@
|
||||
from routers import admin, group, private
|
||||
190
routers/admin.py
Normal file
190
routers/admin.py
Normal file
@@ -0,0 +1,190 @@
|
||||
"""Handlers for admin review/contact groups."""
|
||||
import logging
|
||||
|
||||
from aiogram import F, Router, types
|
||||
from aiogram.filters import Command
|
||||
|
||||
import bot_state as state_store
|
||||
from config import (
|
||||
BOT_USERNAME,
|
||||
CHAT_CONTACT_ADMIN_GROUP_ID,
|
||||
CHATROOM_PRIVATE_BACKUP_GROUP_ID,
|
||||
CHATROOM_SEMIPUBLIC_GROUP_ID,
|
||||
MAIN_PUBLIC_CHANNEL_ID,
|
||||
REVIEW_ADMIN_CHATROOM_SEMIPUBLIC_GROUP_ID,
|
||||
)
|
||||
from hashing import register_file
|
||||
from keyboards import publish_kb
|
||||
from utils import build_quote, get_admin_display_name, send_media_items
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
router = Router(name="admin")
|
||||
|
||||
# ── Submission approve / reject ───────────────────────────────────────────────
|
||||
|
||||
@router.callback_query(F.data.startswith("a|"))
|
||||
async def approve(cb: types.CallbackQuery):
|
||||
sub_id = int(cb.data.split("|")[1])
|
||||
if sub_id not in state_store.submissions:
|
||||
await cb.answer("Submission not found.", show_alert=True)
|
||||
return
|
||||
await cb.message.edit_reply_markup(reply_markup=publish_kb(sub_id))
|
||||
await cb.answer()
|
||||
|
||||
|
||||
@router.callback_query(F.data.startswith("p|"))
|
||||
async def publish(cb: types.CallbackQuery):
|
||||
parts = cb.data.split("|")
|
||||
target = parts[1]
|
||||
sub_id = int(parts[2])
|
||||
submission = state_store.submissions.get(sub_id)
|
||||
if not submission:
|
||||
await cb.answer("Submission not found.", show_alert=True)
|
||||
return
|
||||
|
||||
user_id = submission["user_id"]
|
||||
media = submission["media"]
|
||||
|
||||
duplicate_media = [m for m in media if m.get("file_unique_id", "") in state_store.backup_hashes]
|
||||
unique_media = [m for m in media if m.get("file_unique_id", "") not in state_store.backup_hashes]
|
||||
|
||||
if duplicate_media:
|
||||
try:
|
||||
user_info = await cb.bot.get_chat(user_id)
|
||||
user_ref = (
|
||||
f'@{user_info.username}'
|
||||
if getattr(user_info, "username", None)
|
||||
else str(user_id)
|
||||
)
|
||||
except Exception:
|
||||
user_ref = str(user_id)
|
||||
try:
|
||||
await cb.bot.send_message(
|
||||
REVIEW_ADMIN_CHATROOM_SEMIPUBLIC_GROUP_ID,
|
||||
f"⚠️ Content from {user_ref} contains {len(duplicate_media)} duplicate(s). "
|
||||
"Skipping those.",
|
||||
parse_mode="HTML",
|
||||
)
|
||||
except Exception:
|
||||
logger.warning("Could not send duplicate notice")
|
||||
|
||||
caption = (
|
||||
"This is an anonymous submission reviewed by admins.\n\n"
|
||||
"You can apply having self-harm imagery posted using "
|
||||
f'<a href="https://t.me/{BOT_USERNAME}">this bot</a>.'
|
||||
)
|
||||
|
||||
if unique_media:
|
||||
await send_media_items(cb.bot, CHATROOM_PRIVATE_BACKUP_GROUP_ID, unique_media)
|
||||
for item in unique_media:
|
||||
register_file(item["file_unique_id"])
|
||||
|
||||
if target in {"d", "both"}:
|
||||
await send_media_items(cb.bot, CHATROOM_SEMIPUBLIC_GROUP_ID, unique_media, caption=caption)
|
||||
if target in {"b", "both"}:
|
||||
await send_media_items(cb.bot, MAIN_PUBLIC_CHANNEL_ID, unique_media, caption=caption)
|
||||
|
||||
try:
|
||||
await cb.bot.send_message(user_id, "Your submission was approved ✅")
|
||||
except Exception:
|
||||
logger.warning("Could not notify user %s of approval", user_id)
|
||||
|
||||
state_store.submissions.pop(sub_id, None)
|
||||
try:
|
||||
await cb.message.edit_reply_markup()
|
||||
except Exception:
|
||||
pass
|
||||
await cb.answer()
|
||||
|
||||
|
||||
@router.callback_query(F.data.startswith("r|"))
|
||||
async def reject(cb: types.CallbackQuery):
|
||||
sub_id = int(cb.data.split("|")[1])
|
||||
submission = state_store.submissions.get(sub_id)
|
||||
if not submission:
|
||||
await cb.answer("Submission not found.", show_alert=True)
|
||||
return
|
||||
try:
|
||||
await cb.bot.send_message(submission["user_id"], "Your submission was rejected ❌")
|
||||
except Exception:
|
||||
logger.warning("Could not notify user %s of rejection", submission["user_id"])
|
||||
state_store.submissions.pop(sub_id, None)
|
||||
try:
|
||||
await cb.message.edit_reply_markup()
|
||||
except Exception:
|
||||
pass
|
||||
await cb.answer()
|
||||
|
||||
# ── Admin contact-group reply commands ────────────────────────────────────────
|
||||
|
||||
@router.message(F.chat.id == CHAT_CONTACT_ADMIN_GROUP_ID, F.reply_to_message, Command("stop"))
|
||||
async def admin_stop_chat(message: types.Message):
|
||||
route = state_store.chat_message_map.get(message.reply_to_message.message_id)
|
||||
if not route:
|
||||
return
|
||||
user_id = route["user_id"]
|
||||
if state_store.chat_sessions.pop(user_id, None):
|
||||
try:
|
||||
await message.bot.send_message(user_id, "An admin has closed this chat.")
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
|
||||
@router.message(F.chat.id == CHAT_CONTACT_ADMIN_GROUP_ID, F.reply_to_message, Command("ban"))
|
||||
async def admin_ban_user(message: types.Message):
|
||||
route = state_store.chat_message_map.get(message.reply_to_message.message_id)
|
||||
if not route:
|
||||
return
|
||||
user_id = route["user_id"]
|
||||
state_store.banned_chat_users.add(user_id)
|
||||
if state_store.chat_sessions.pop(user_id, None):
|
||||
try:
|
||||
await message.bot.send_message(user_id, "An admin has closed this chat.")
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
|
||||
@router.message(F.chat.id == CHAT_CONTACT_ADMIN_GROUP_ID, F.reply_to_message, Command("private"))
|
||||
async def admin_private_chat(message: types.Message):
|
||||
route = state_store.chat_message_map.get(message.reply_to_message.message_id)
|
||||
if not route:
|
||||
return
|
||||
user_id = route["user_id"]
|
||||
username = (
|
||||
f"@{message.from_user.username}"
|
||||
if message.from_user.username
|
||||
else message.from_user.full_name
|
||||
)
|
||||
try:
|
||||
await message.bot.send_message(
|
||||
user_id,
|
||||
f"An admin wants to continue this chat privately. Feel free to text them at {username}, "
|
||||
"or open a new chat here. This chat is now closed.",
|
||||
)
|
||||
except Exception:
|
||||
pass
|
||||
state_store.chat_sessions.pop(user_id, None)
|
||||
|
||||
|
||||
@router.message(F.chat.id == CHAT_CONTACT_ADMIN_GROUP_ID, F.reply_to_message)
|
||||
async def admin_reply(message: types.Message):
|
||||
if message.text in {"/stop", "/ban", "/private"}:
|
||||
return
|
||||
route = state_store.chat_message_map.get(message.reply_to_message.message_id)
|
||||
if not route:
|
||||
return
|
||||
user_id = route["user_id"]
|
||||
if user_id not in state_store.chat_sessions:
|
||||
return
|
||||
quote = build_quote(route["quote"])
|
||||
admin_name = get_admin_display_name(message.from_user)
|
||||
try:
|
||||
if message.text:
|
||||
await message.bot.send_message(user_id, f"{admin_name}\n{quote}\n\n{message.text}")
|
||||
else:
|
||||
await message.bot.send_message(user_id, f"{admin_name}\n{quote}")
|
||||
await message.bot.copy_message(
|
||||
user_id, CHAT_CONTACT_ADMIN_GROUP_ID, message.message_id
|
||||
)
|
||||
except Exception:
|
||||
logger.exception("Failed to relay admin reply to user %s", user_id)
|
||||
186
routers/group.py
Normal file
186
routers/group.py
Normal file
@@ -0,0 +1,186 @@
|
||||
"""Handlers for the semipublic chatroom and backup group."""
|
||||
import asyncio
|
||||
import logging
|
||||
|
||||
from aiogram import F, Router, types
|
||||
from aiogram.filters import Command
|
||||
|
||||
import bot_state as state_store
|
||||
from config import (
|
||||
BLACKLIST_MODE,
|
||||
CHATROOM_PRIVATE_BACKUP_GROUP_ID,
|
||||
CHATROOM_SEMIPUBLIC_GROUP_ID,
|
||||
PURGE_INTERVAL_HOURS,
|
||||
INVITELINK_ARCH,
|
||||
INVITELINK_CHAT,
|
||||
BOT_USERNAME,
|
||||
)
|
||||
from filters import contains_link, process_blacklisted_message
|
||||
from hashing import register_file
|
||||
from utils import is_group_admin
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
router = Router(name="group")
|
||||
|
||||
# ── Background purge task ─────────────────────────────────────────────────────
|
||||
|
||||
async def purge_chatroom_semipublic_group(bot) -> None:
|
||||
while True:
|
||||
await asyncio.sleep(PURGE_INTERVAL_HOURS * 3600)
|
||||
for message_id, is_bot_msg in list(state_store.chatroom_semipublic_group_messages.items()):
|
||||
if is_bot_msg:
|
||||
continue
|
||||
try:
|
||||
await bot.delete_message(CHATROOM_SEMIPUBLIC_GROUP_ID, message_id)
|
||||
except Exception:
|
||||
pass
|
||||
state_store.chatroom_semipublic_group_messages.pop(message_id, None)
|
||||
|
||||
# ── Backup-group tracker ──────────────────────────────────────────────────────
|
||||
|
||||
@router.message(F.chat.id == CHATROOM_PRIVATE_BACKUP_GROUP_ID)
|
||||
async def track_backup_group(message: types.Message):
|
||||
if message.photo:
|
||||
register_file(message.photo[-1].file_unique_id)
|
||||
elif message.video:
|
||||
register_file(message.video.file_unique_id)
|
||||
elif message.document:
|
||||
register_file(message.document.file_unique_id)
|
||||
|
||||
|
||||
@router.message(
|
||||
F.chat.id == CHATROOM_PRIVATE_BACKUP_GROUP_ID,
|
||||
Command("reindex"),
|
||||
)
|
||||
async def reindex_backup(message: types.Message):
|
||||
if not await is_group_admin(message.bot, CHATROOM_PRIVATE_BACKUP_GROUP_ID, message.from_user.id):
|
||||
return
|
||||
count = len(state_store.backup_hashes)
|
||||
await message.reply(
|
||||
f"ℹ️ Currently <b>{count}</b> files indexed in the hash cache.\n\n"
|
||||
"The Bot API does not expose a bulk message history endpoint. "
|
||||
"To do a full historical reindex, forward all older media back into "
|
||||
"this group — the bot will register each file automatically as it arrives.",
|
||||
parse_mode="HTML",
|
||||
)
|
||||
|
||||
# ── Welcome / join request ────────────────────────────────────────────────────
|
||||
|
||||
@router.message(F.new_chat_members)
|
||||
async def welcome(message: types.Message):
|
||||
async with state_store.welcome_lock:
|
||||
try:
|
||||
await message.delete()
|
||||
except Exception:
|
||||
pass
|
||||
for mid in state_store.welcome_messages.get(message.chat.id, []):
|
||||
try:
|
||||
await message.bot.delete_message(message.chat.id, mid)
|
||||
except Exception:
|
||||
pass
|
||||
sent = []
|
||||
for user in message.new_chat_members:
|
||||
mention = (
|
||||
f'@{user.username}' if user.username else str(user.id)
|
||||
)
|
||||
text = (
|
||||
f"{mention} welcome to the official s3lfharm archive.\n\n"
|
||||
"You can view media by checking pinned messages or the media section.\n\n"
|
||||
"Feel free to share your own s3lfharm imagery using "
|
||||
f'<a href="https://t.me/{BOT_USERNAME}">this bot</a>.\n\n'
|
||||
f'\U0001f48b <a href="{INVITELINK_ARCH}">Join the public archive</a>\n\n'
|
||||
f'<blockquote>S3LF HARM</blockquote>'
|
||||
)
|
||||
try:
|
||||
msg = await message.bot.send_message(
|
||||
message.chat.id,
|
||||
text,
|
||||
parse_mode="HTML",
|
||||
disable_web_page_preview=True,
|
||||
)
|
||||
sent.append(msg.message_id)
|
||||
except Exception:
|
||||
logger.exception("Failed to send welcome in %s", message.chat.id)
|
||||
state_store.welcome_messages[message.chat.id] = sent
|
||||
|
||||
|
||||
@router.chat_join_request()
|
||||
async def auto_approve_join_request(request: types.ChatJoinRequest):
|
||||
try:
|
||||
await request.bot.approve_chat_join_request(request.chat.id, request.from_user.id)
|
||||
logger.info("Approved user %s to join %s", request.from_user.id, request.chat.id)
|
||||
except Exception:
|
||||
logger.exception("Failed to approve join request from %s", request.from_user.id)
|
||||
|
||||
# ── Auto-delete Telegram service/system messages in semipublic group ──────────
|
||||
|
||||
@router.message(
|
||||
F.chat.id == CHATROOM_SEMIPUBLIC_GROUP_ID,
|
||||
F.content_type.in_({
|
||||
types.ContentType.NEW_CHAT_MEMBERS,
|
||||
types.ContentType.LEFT_CHAT_MEMBER,
|
||||
types.ContentType.NEW_CHAT_TITLE,
|
||||
types.ContentType.NEW_CHAT_PHOTO,
|
||||
types.ContentType.DELETE_CHAT_PHOTO,
|
||||
types.ContentType.GROUP_CHAT_CREATED,
|
||||
types.ContentType.SUPERGROUP_CHAT_CREATED,
|
||||
types.ContentType.MESSAGE_AUTO_DELETE_TIMER_CHANGED,
|
||||
types.ContentType.PINNED_MESSAGE,
|
||||
}),
|
||||
)
|
||||
async def delete_service_messages(message: types.Message):
|
||||
try:
|
||||
await message.delete()
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
# ── Semipublic group moderation ───────────────────────────────────────────────
|
||||
|
||||
@router.message(F.chat.id == CHATROOM_SEMIPUBLIC_GROUP_ID)
|
||||
async def handle_semipublic_message(message: types.Message):
|
||||
state_store.chatroom_semipublic_group_messages[message.message_id] = bool(
|
||||
message.from_user and message.from_user.is_bot
|
||||
)
|
||||
|
||||
if not message.from_user or message.from_user.is_bot:
|
||||
return
|
||||
|
||||
# ── Define text first ────────────────────────────────────────────────────
|
||||
text = message.text or message.caption or ""
|
||||
|
||||
# ── Blacklist check (all users including admins) ──────────────────────────
|
||||
censored_text, was_censored = process_blacklisted_message(text)
|
||||
if was_censored:
|
||||
try:
|
||||
await message.delete()
|
||||
except Exception:
|
||||
pass
|
||||
sender = (
|
||||
f'@{message.from_user.username}'
|
||||
if message.from_user.username
|
||||
else str(message.from_user.id)
|
||||
)
|
||||
if BLACKLIST_MODE == 1:
|
||||
try:
|
||||
await message.bot.send_message(
|
||||
CHATROOM_SEMIPUBLIC_GROUP_ID,
|
||||
f"Censored text from {sender} →\n<blockquote>{censored_text}</blockquote>",
|
||||
parse_mode="HTML",
|
||||
)
|
||||
except Exception:
|
||||
logger.exception("Failed to send censor notice")
|
||||
return
|
||||
|
||||
# ── Link check — admins are fully exempt ─────────────────────────────────
|
||||
if await is_group_admin(message.bot, CHATROOM_SEMIPUBLIC_GROUP_ID, message.from_user.id):
|
||||
return # admin: keep message as-is, no further checks
|
||||
|
||||
has_link_entity = any(
|
||||
e.type in ("url", "text_link")
|
||||
for e in (message.entities or []) + (message.caption_entities or [])
|
||||
)
|
||||
if has_link_entity or contains_link(text):
|
||||
try:
|
||||
await message.delete()
|
||||
except Exception:
|
||||
pass
|
||||
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