Spaces:
Running on Zero
Running on Zero
Download app/bot/handlers/admin_queue.py from cipher-attacks/Dbu-central-bot: direct link, hf CLI and curl.
- Browser
- Download file 9.52 kB
-
https://huggingface.co/spaces/cipher-attacks/Dbu-central-bot/resolve/main/app/bot/handlers/admin_queue.py
- Command line
-
hf download hf://spaces/cipher-attacks/Dbu-central-bot/app/bot/handlers/admin_queue.py
-
curl -L -o admin_queue.py https://huggingface.co/spaces/cipher-attacks/Dbu-central-bot/resolve/main/app/bot/handlers/admin_queue.py
9.52 kB
| """Admin review of student files: one card at a time, three taps, no per-file spam.""" | |
| import html | |
| from typing import Optional | |
| from aiogram import F, Router | |
| from aiogram.filters import Command | |
| from aiogram.types import CallbackQuery, InlineKeyboardButton, InlineKeyboardMarkup, Message | |
| from sqlalchemy.ext.asyncio import AsyncSession | |
| from app.bot.handlers.admin import IsAdmin | |
| from app.db.models import Submission | |
| from app.db.repos import library as lib_repo | |
| from app.db.repos import submissions as sub_repo | |
| from app.db.repos import users as users_repo | |
| from app.services import classifier as C | |
| from app.services import contrib_rules as K | |
| from app.services import contributions as contrib | |
| from app.services import library as lib | |
| router = Router() | |
| router.message.filter(IsAdmin()) | |
| router.callback_query.filter(IsAdmin()) | |
| _skipped: dict[int, set[int]] = {} # admin_id -> submissions skipped in this review round | |
| PAGE = 8 | |
| REASON_LABEL = {"duplicate": "β»οΈ Duplicate", "quality": "π Low quality", "notallowed": "π« Not allowed"} | |
| def _b(text: str, data: str) -> InlineKeyboardButton: | |
| return InlineKeyboardButton(text=text, callback_data=data) | |
| def _course_label(c) -> str: | |
| return f"{c.code + ' ' if c.code else ''}{c.name}" if c else "β no course yet" | |
| def _mb(size: Optional[int]) -> str: | |
| return f"{size / 1048576:.1f} MB" if size else "?" | |
| async def _card(session: AsyncSession, sub: Submission) -> Optional[tuple[str, InlineKeyboardMarkup]]: | |
| material = await lib_repo.get_material(session, sub.material_id) if sub.material_id else None | |
| if material is None: | |
| return None | |
| user = await users_repo.get(session, sub.user_id) | |
| course = await lib_repo.get_course(session, material.course_id) if material.course_id else None | |
| approved = await sub_repo.approved_count(session, sub.user_id) | |
| left = await sub_repo.pending_total(session) | |
| who = html.escape((user.full_name if user else "?") or "?") | |
| handle = f" @{html.escape(user.username)}" if user and user.username else "" | |
| text = ( | |
| f"π₯ <b>Submission #{sub.id}</b> Β· {left} waiting\n" | |
| f"π€ {who}{handle} Β· id{sub.user_id} Β· {approved} approved before\n" | |
| f"π <i>{html.escape((sub.file_name or material.source_name or '?')[:70])}</i> Β· {_mb(material.size)}" | |
| f"{f' Β· {material.pages} p' if material.pages else ''}\n" | |
| f"π {html.escape(_course_label(course))} Β· <b>{material.kind}</b>\nβ{html.escape(material.title)}β" | |
| ) | |
| if sub.note: | |
| text += f"\nβ οΈ {html.escape(sub.note)}" | |
| rows = [ | |
| [_b("β Accept", f"qq:a:{sub.id}")], | |
| [_b("βοΈ Course", f"qq:c:{sub.id}"), _b("π Reject", f"qq:r:{sub.id}")], | |
| ] | |
| if sub.dup_of: | |
| rows.append([_b(f"β»οΈ Replace #{sub.dup_of} with this", f"qq:p:{sub.id}")]) | |
| rows.append([_b("β Skip", f"qq:k:{sub.id}")]) | |
| return text, InlineKeyboardMarkup(inline_keyboard=rows) | |
| async def show_next(bot, session: AsyncSession, admin_id: int, chat_id: int) -> None: | |
| """Send the next waiting card (or say the queue is empty).""" | |
| skip = _skipped.setdefault(admin_id, set()) | |
| for _ in range(25): # cards whose file vanished are cleaned up on the way | |
| sub = await sub_repo.next_pending(session, skip) | |
| if sub is None: | |
| _skipped.pop(admin_id, None) | |
| await bot.send_message(chat_id, "π Nothing is waiting for review.") | |
| return | |
| card = await _card(session, sub) | |
| if card: | |
| await bot.send_message(chat_id, card[0], reply_markup=card[1]) | |
| return | |
| await contrib.reject(bot, session, sub, "quality") # file gone: close it silently | |
| async def _finish(call: CallbackQuery, session: AsyncSession, text: str) -> None: | |
| """Replace the card with the result, then bring the next one.""" | |
| if call.message: | |
| try: | |
| await call.message.edit_text(text) | |
| except Exception: | |
| pass | |
| await show_next(call.bot, session, call.from_user.id, call.message.chat.id) | |
| async def _pending(call: CallbackQuery, session: AsyncSession, sid: int) -> Optional[Submission]: | |
| sub = await sub_repo.get(session, sid) | |
| if sub is None or sub.status != "pending": | |
| await call.answer("Already handled.", show_alert=True) | |
| return None | |
| return sub | |
| async def _rerender(call: CallbackQuery, session: AsyncSession, sub: Submission) -> None: | |
| card = await _card(session, sub) | |
| if card and call.message: | |
| try: | |
| await call.message.edit_text(card[0], reply_markup=card[1]) | |
| except Exception: | |
| pass | |
| async def queue(message: Message, session: AsyncSession) -> None: | |
| _skipped.pop(message.from_user.id, None) | |
| await show_next(message.bot, session, message.from_user.id, message.chat.id) | |
| async def start(call: CallbackQuery, session: AsyncSession) -> None: | |
| await call.answer() | |
| _skipped.pop(call.from_user.id, None) | |
| await show_next(call.bot, session, call.from_user.id, call.message.chat.id) | |
| async def accept(call: CallbackQuery, session: AsyncSession) -> None: | |
| sub = await _pending(call, session, int(call.data.split(":")[2])) | |
| if not sub: | |
| return | |
| decision = await contrib.approve(call.bot, session, sub) | |
| if not decision.ok: | |
| await call.answer(decision.text, show_alert=True) | |
| return | |
| await call.answer() | |
| await _finish(call, session, decision.text) | |
| async def replace(call: CallbackQuery, session: AsyncSession) -> None: | |
| sub = await _pending(call, session, int(call.data.split(":")[2])) | |
| if not sub: | |
| return | |
| decision = await contrib.approve(call.bot, session, sub, replace_id=sub.dup_of) | |
| if not decision.ok: | |
| await call.answer(decision.text, show_alert=True) | |
| return | |
| await call.answer() | |
| await _finish(call, session, decision.text + f"\n(replaced #{sub.dup_of})") | |
| async def reject_menu(call: CallbackQuery, session: AsyncSession) -> None: | |
| sub = await _pending(call, session, int(call.data.split(":")[2])) | |
| if not sub: | |
| return | |
| rows = [[_b(REASON_LABEL[r], f"qq:x:{sub.id}:{r}")] for r in K.REJECT_REASONS] | |
| rows.append([_b("β© Back", f"qq:b:{sub.id}")]) | |
| await call.answer() | |
| await call.message.edit_reply_markup(reply_markup=InlineKeyboardMarkup(inline_keyboard=rows)) | |
| async def reject(call: CallbackQuery, session: AsyncSession) -> None: | |
| _, _, sid, reason = call.data.split(":") | |
| sub = await _pending(call, session, int(sid)) | |
| if not sub: | |
| return | |
| decision = await contrib.reject(call.bot, session, sub, reason) | |
| await call.answer() | |
| await _finish(call, session, decision.text) | |
| async def back(call: CallbackQuery, session: AsyncSession) -> None: | |
| sub = await _pending(call, session, int(call.data.split(":")[2])) | |
| if sub: | |
| await call.answer() | |
| await _rerender(call, session, sub) | |
| async def skip(call: CallbackQuery, session: AsyncSession) -> None: | |
| sid = int(call.data.split(":")[2]) | |
| _skipped.setdefault(call.from_user.id, set()).add(sid) | |
| await call.answer() | |
| await _finish(call, session, f"β Skipped #{sid} (use /queue to see it again).") | |
| async def course_menu(call: CallbackQuery, session: AsyncSession) -> None: | |
| sub = await _pending(call, session, int(call.data.split(":")[2])) | |
| if not sub: | |
| return | |
| ranked = C.rank_courses(await lib.all_courses_cached(session), [(sub.file_name or "", "name")]) | |
| rows = [[_b(f"π {_course_label(c)}"[:60], f"qq:s:{sub.id}:{c.id}")] for _, c in ranked[:5]] | |
| rows.append([_b("π All courses", f"qq:l:{sub.id}:0")]) | |
| rows.append([_b("β© Back", f"qq:b:{sub.id}")]) | |
| await call.answer() | |
| await call.message.edit_reply_markup(reply_markup=InlineKeyboardMarkup(inline_keyboard=rows)) | |
| async def all_courses(call: CallbackQuery, session: AsyncSession) -> None: | |
| _, _, sid, page = call.data.split(":") | |
| sub = await _pending(call, session, int(sid)) | |
| if not sub: | |
| return | |
| page = int(page) | |
| courses = await lib.all_courses_cached(session) | |
| rows = [[_b(f"π {_course_label(c)}"[:60], f"qq:s:{sub.id}:{c.id}")] for c in courses[page * PAGE : (page + 1) * PAGE]] | |
| nav = [] | |
| if page > 0: | |
| nav.append(_b("β¬ οΈ", f"qq:l:{sub.id}:{page - 1}")) | |
| if (page + 1) * PAGE < len(courses): | |
| nav.append(_b("β‘οΈ", f"qq:l:{sub.id}:{page + 1}")) | |
| if nav: | |
| rows.append(nav) | |
| rows.append([_b("β© Back", f"qq:b:{sub.id}")]) | |
| await call.answer() | |
| await call.message.edit_reply_markup(reply_markup=InlineKeyboardMarkup(inline_keyboard=rows)) | |
| async def set_course(call: CallbackQuery, session: AsyncSession) -> None: | |
| _, _, sid, cid = call.data.split(":") | |
| sub = await _pending(call, session, int(sid)) | |
| material = await lib_repo.get_material(session, sub.material_id) if sub else None | |
| if not sub or not material: | |
| return | |
| material.course_id = int(cid) | |
| await call.answer("Course set") | |
| await _rerender(call, session, sub) | |