Spaces:
Running on Zero
Running on Zero
Download app/services/contributions.py from cipher-attacks/Dbu-central-bot: direct link, hf CLI and curl.
- Browser
- Download file 8.38 kB
-
https://huggingface.co/spaces/cipher-attacks/Dbu-central-bot/resolve/main/app/services/contributions.py
- Command line
-
hf download hf://spaces/cipher-attacks/Dbu-central-bot/app/services/contributions.py
-
curl -L -o contributions.py https://huggingface.co/spaces/cipher-attacks/Dbu-central-bot/resolve/main/app/services/contributions.py
8.38 kB
| """Student contributions: submit -> admin review -> approve (stamp, publish, reward) or reject (explain).""" | |
| import logging | |
| from dataclasses import dataclass, field | |
| from datetime import datetime, timedelta, timezone | |
| from typing import Optional | |
| from aiogram import Bot | |
| from sqlalchemy.ext.asyncio import AsyncSession | |
| from app.config import get_settings | |
| from app.core import season | |
| from app.core.i18n import t | |
| from app.core.notify import safe_send | |
| from app.db.models import Material, Submission, User | |
| 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.db.repos import xp as xp_repo | |
| from app.services import contrib_rules as K | |
| from app.services import library as library_service | |
| from app.services import smart_ingest | |
| from app.services import xp as xp_service | |
| log = logging.getLogger(__name__) | |
| # ------------------------------------------------------------------ student side | |
| async def check_eligibility(session: AsyncSession, user: User) -> Optional[str]: | |
| """None = may submit now, else a reason code (new | blocked | pending | daily).""" | |
| cfg = get_settings() | |
| now = datetime.now(timezone.utc) | |
| rejected = await sub_repo.rejected_times(session, user.id, now - timedelta(days=K.REJECTION_WINDOW_DAYS)) | |
| return K.eligibility( | |
| active_days=user.active_days or 0, | |
| min_days=cfg.contribution_min_days, | |
| pending=await sub_repo.pending_count(session, user.id), | |
| pending_limit=cfg.contribution_pending_limit, | |
| today=await sub_repo.count_since(session, user.id, now - timedelta(hours=24)), | |
| daily_limit=cfg.contribution_daily_limit, | |
| blocked=K.blocked_until(rejected, now), | |
| ) | |
| class SubmitResult: | |
| kind: str # duplicate | accepted | need_course | |
| submission: Optional[Submission] = None | |
| material: Optional[Material] = None | |
| outcome: Optional[smart_ingest.Outcome] = None | |
| duplicate_of: Optional[Material] = None | |
| suggestions: list = field(default_factory=list) | |
| async def submit_file( | |
| bot: Bot, session: AsyncSession, user: User, *, file_id: str, unique_id: str, | |
| file_name: Optional[str], size: Optional[int], caption: Optional[str], forced_course=None, | |
| ) -> SubmitResult: | |
| out = await smart_ingest.ingest_file( | |
| bot, session, uploader_id=user.id, file_type="document", file_id=file_id, unique_id=unique_id, | |
| file_name=file_name, size=size, caption=caption, forced_course=forced_course, | |
| backup_channel=0, message=None, stamp=False, # stamped only when an admin approves it | |
| ) | |
| if out.status == "duplicate": | |
| return SubmitResult("duplicate", duplicate_of=out.duplicate_of, outcome=out) | |
| material = out.material | |
| material.status = "pending" # invisible to students until approved | |
| flags = list(out.notes) | |
| dup_of = None | |
| if out.status == "near_duplicate" and out.duplicate_of is not None: | |
| dup_of = out.duplicate_of.id | |
| flags.append(f"near-duplicate of #{dup_of} β{out.duplicate_of.title[:40]}β") | |
| if material.course_id is None: | |
| flags.append("course unknown") | |
| sub = await sub_repo.create( | |
| session, user_id=user.id, material_id=material.id, file_name=file_name, note="; ".join(flags), dup_of=dup_of | |
| ) | |
| kind = "need_course" if material.course_id is None else "accepted" | |
| return SubmitResult(kind, submission=sub, material=material, outcome=out, suggestions=out.suggestions) | |
| async def wanted_courses(session: AsyncSession, user: User, limit: int = 8) -> list[tuple[object, int]]: | |
| """Courses (for this student's stream/year) with the fewest files first, only those short of material.""" | |
| counts = await lib_repo.file_counts(session) | |
| courses = await lib_repo.list_courses(session, user.stream, user.year, 0, 200, only_with_files=False) | |
| short = [(c, counts.get(c.id, 0)) for c in courses if counts.get(c.id, 0) < K.SPARSE_COURSE_FILES] | |
| short.sort(key=lambda item: (item[1], item[0].id)) | |
| return short[:limit] | |
| # ------------------------------------------------------------------ admin side | |
| class Decision: | |
| ok: bool | |
| text: str | |
| async def approve( | |
| bot: Bot, session: AsyncSession, sub: Submission, *, replace_id: Optional[int] = None | |
| ) -> Decision: | |
| cfg = get_settings() | |
| material = await lib_repo.get_material(session, sub.material_id) if sub.material_id else None | |
| if material is None: | |
| sub.status, sub.reviewed_at, sub.reason = "rejected", datetime.now(timezone.utc), "gone" | |
| return Decision(False, "That file no longer exists.") | |
| if material.course_id is None: | |
| return Decision(False, "Pick a course first.") | |
| submitter = await users_repo.get(session, sub.user_id) | |
| files_before = await lib_repo.ok_file_count(session, material.course_id) | |
| stamp_note = "" | |
| if cfg.watermark_enabled and (material.source_name or "").lower().endswith(".pdf") and not material.watermarked: | |
| target = cfg.library_channel_id or (next(iter(cfg.admin_id_set), None) or sub.user_id) | |
| ok, reason = await smart_ingest.stamp_existing(bot, material, target) | |
| stamp_note = " Β· π stamped" if ok else f" Β· not stamped ({reason})" | |
| if replace_id: | |
| old = await lib_repo.get_material(session, replace_id) | |
| if old: | |
| material.downloads = (material.downloads or 0) + (old.downloads or 0) | |
| await lib_repo.delete_material(session, old.id) | |
| material.status = "ok" | |
| if submitter and submitter.credit_name: | |
| material.credit = (submitter.full_name or "").strip().split(" ")[0][:40] or None | |
| gained = 0 | |
| badge_tier = None | |
| if submitter: | |
| rewarded = await xp_repo.count_kind_in_week(session, submitter.id, "contribution", season.week_key()) | |
| xp, bonus = K.reward( | |
| rewarded_this_week=rewarded, course_files_before=files_before, xp=cfg.contribution_xp, | |
| bonus=cfg.contribution_bonus_xp, weekly_cap=cfg.contribution_weekly_cap, | |
| ) | |
| main = await xp_service.award(session, submitter, "contribution", xp, ref=f"sub{sub.id}", capped=False) | |
| extra = await xp_service.award(session, submitter, "contribution_bonus", bonus, ref=f"subb{sub.id}", capped=False) | |
| gained = main.amount + extra.amount | |
| session.sync_session.info.setdefault("invalidate", set()).add(submitter.id) | |
| sub.status, sub.reviewed_at, sub.xp = "approved", datetime.now(timezone.utc), gained | |
| session.sync_session.info["wrote"] = True | |
| await session.flush() | |
| library_service.invalidate_courses() | |
| if submitter: | |
| approved_total = await sub_repo.approved_count(session, submitter.id) | |
| course = await lib_repo.get_course(session, material.course_id) | |
| await safe_send( | |
| bot, submitter.id, | |
| t(submitter.lang, "contrib_approved", title=material.title, course=course.name if course else "", xp=gained), | |
| ) | |
| badge_tier = K.badge_for(approved_total) if approved_total in {n for n, _ in K.BADGES} else None | |
| if badge_tier: | |
| await safe_send(bot, submitter.id, t(submitter.lang, "contrib_badge", badge=t(submitter.lang, f"badge_{badge_tier}"))) | |
| return Decision(True, f"β Approved #{material.id} β{material.title}β (+{gained} XP{stamp_note})") | |
| async def reject(bot: Bot, session: AsyncSession, sub: Submission, reason: str) -> Decision: | |
| reason = reason if reason in K.REJECT_REASONS else "quality" | |
| material = await lib_repo.get_material(session, sub.material_id) if sub.material_id else None | |
| title = material.title if material else (sub.file_name or "") | |
| if material: | |
| await lib_repo.delete_material(session, material.id) | |
| now = datetime.now(timezone.utc) | |
| sub.status, sub.reason, sub.reviewed_at = "rejected", reason, now | |
| session.sync_session.info["wrote"] = True | |
| await session.flush() | |
| submitter = await users_repo.get(session, sub.user_id) | |
| if submitter: | |
| text = t(submitter.lang, f"contrib_rejected_{reason}", title=title) | |
| rejected = await sub_repo.rejected_times(session, submitter.id, now - timedelta(days=K.REJECTION_WINDOW_DAYS)) | |
| if K.blocked_until(rejected, now): | |
| text += t(submitter.lang, "contrib_blocked_notice") | |
| await safe_send(bot, submitter.id, text) | |
| return Decision(True, f"π Rejected ({reason}): β{title}β") | |