feat(bot): add strict whitelist auth bypass, multi-STT consensus pipeline, and applicant logging

This commit is contained in:
2026-10-08 23:24:17 +03:30
parent 77c0cacbb6
commit 19c3055008
15 changed files with 3172 additions and 40 deletions
+53 -20
View File
@@ -39,6 +39,10 @@ async def handle_update(ctx: BotContext, update: dict) -> None:
async def handle_message(ctx: BotContext, chat_id: int, message: dict) -> None:
user = await _gate(ctx, chat_id, message)
if user is None:
return
text = (message.get("text") or message.get("caption") or "").strip()
source = "text"
voice_transcripts: list[dict] | None = None
@@ -56,10 +60,6 @@ async def handle_message(ctx: BotContext, chat_id: int, message: dict) -> None:
return
ctx.pending.pop(chat_id) # stale/irrelevant flow → fall through
user = await _gate(ctx, chat_id, message)
if user is None:
return # gate either started linking or is mid-linking
reply_to = message.get("reply_to_message")
reply_to_msg_id = reply_to.get("message_id") if reply_to else None
@@ -272,25 +272,58 @@ async def handle_message(ctx: BotContext, chat_id: int, message: dict) -> None:
# helpers
# ---------------------------------------------------------------------------
from datetime import datetime, timezone
import json
from pathlib import Path
UNREGISTERED_FILE = Path(__file__).resolve().parent.parent / "unregistered_users.json"
def _record_unregistered_user(chat_id: int, message: dict) -> None:
from_user = message.get("from") or {}
record = {
"chat_id": chat_id,
"username": from_user.get("username"),
"first_name": from_user.get("first_name"),
"last_name": from_user.get("last_name"),
"timestamp": datetime.now(timezone.utc).isoformat(),
"text_preview": (message.get("text") or "")[:100],
}
records = []
try:
if UNREGISTERED_FILE.exists():
content = UNREGISTERED_FILE.read_text(encoding="utf-8").strip()
if content:
records = json.loads(content)
if not isinstance(records, list):
records = []
except Exception:
records = []
records.append(record)
try:
UNREGISTERED_FILE.write_text(json.dumps(records, ensure_ascii=False, indent=2), encoding="utf-8")
except Exception as e:
logger.warning("Could not write to unregistered_users.json: %s", e)
async def _gate(ctx: BotContext, chat_id: int, message: dict) -> LinkedUser | None:
"""Link or admin gate. Returns None when the message was consumed by linking."""
from . import links
"""Whitelist gate (Casdoor bypassed). Unregistered users are blocked and logged."""
from app.defaults import AUTHORIZED_USERS
link = await ctx.links.get(chat_id)
if link and link.user_id:
return LinkedUser(chat_id, link.user_id, link.display_name, link.role, is_admin=False)
if chat_id in ctx.admin_chat_ids:
return LinkedUser(chat_id, None, None, None, is_admin=True)
# 1. Whitelist check (only registered whitelist IDs)
if chat_id in AUTHORIZED_USERS:
u = AUTHORIZED_USERS[chat_id]
return LinkedUser(
chat_id=chat_id,
user_id=u.get("user_id"),
display_name=u.get("display_name"),
role=u.get("role", "procurement_manager"),
is_admin=u.get("is_admin", True),
)
flow = ctx.pending.get(chat_id)
if flow and flow.kind == "link_password":
await links.finish_link(ctx, chat_id, flow, text=None, password=message.get("text") or "")
return None
if flow and flow.kind == "link_username":
await links.finish_username_step(ctx, chat_id, flow, message.get("text") or "")
return None
await ctx.send(chat_id, LINK_START)
ctx.pending.set(chat_id, "link_username", {})
# 2. Block unregistered user and record log
_record_unregistered_user(chat_id, message)
await ctx.send(chat_id, "دسترسی شما هنوز تعریف نشده است. درخواست شما جهت بررسی ثبت شد.")
return None
+12 -6
View File
@@ -29,12 +29,18 @@ async def handle_callback(ctx, cq: dict) -> None:
async def _gate(ctx, chat_id: int) -> LinkedUser | None:
link = await ctx.links.get(chat_id)
if link and link.user_id:
return LinkedUser(chat_id, link.user_id, link.display_name, link.role, is_admin=False)
if chat_id in ctx.admin_chat_ids:
return LinkedUser(chat_id, None, None, None, is_admin=True)
await ctx.send(chat_id, UNLINKED)
from app.defaults import AUTHORIZED_USERS
if chat_id in AUTHORIZED_USERS:
u = AUTHORIZED_USERS[chat_id]
return LinkedUser(
chat_id=chat_id,
user_id=u.get("user_id"),
display_name=u.get("display_name"),
role=u.get("role", "procurement_manager"),
is_admin=u.get("is_admin", True),
)
await ctx.send(chat_id, "دسترسی شما هنوز تعریف نشده است. درخواست شما جهت بررسی ثبت شد.")
return None
+13 -3
View File
@@ -31,12 +31,15 @@ async def handle_command(ctx, chat_id: int, text: str) -> None:
async def _start(ctx, chat_id: int) -> None:
from app.defaults import AUTHORIZED_USERS
await ctx.send(chat_id, WELCOME)
if chat_id in AUTHORIZED_USERS or chat_id in ctx.admin_chat_ids:
return
link = await ctx.links.get(chat_id)
if not link or not link.user_id:
if chat_id not in ctx.admin_chat_ids:
await ctx.send(chat_id, "برای شروع، حساب خود را متصل کن — نام کاربری Casdoor را بفرست:")
ctx.pending.set(chat_id, "link_username", {})
await ctx.send(chat_id, "برای شروع، حساب خود را متصل کن — نام کاربری Casdoor را بفرست:")
ctx.pending.set(chat_id, "link_username", {})
async def _drafts(ctx, chat_id: int) -> None:
@@ -71,6 +74,13 @@ async def _cancel(ctx, chat_id: int) -> None:
async def _whoami(ctx, chat_id: int) -> None:
from app.defaults import AUTHORIZED_USERS
if chat_id in AUTHORIZED_USERS:
u = AUTHORIZED_USERS[chat_id]
role = ROLE_FA.get(u.get("role") or "", u.get("role") or "مدیر تدارکات")
await ctx.send(chat_id, f"اتصال (لیست سفید): {esc(u.get('display_name'))} — {role}")
return
link = await ctx.links.get(chat_id)
if chat_id in ctx.admin_chat_ids:
await ctx.send(chat_id, "حالت ادمین (بدون اتصال Casdoor).")