feat: implement basic command handlers, orchestrator, database manager, and account binding UI

This commit is contained in:
2569718930@qq.com
2026-05-19 17:47:51 +08:00
parent 89914a296b
commit ac4f70e33d
5 changed files with 335 additions and 64 deletions
+98
View File
@@ -4,11 +4,15 @@ import os
from typing import Any
from typing import Callable
from loguru import logger # type: ignore
from src.bot.command_parser import extract_command_name
from src.bot.io_layer import BotIOLayer
from src.bot.observability import CommandTrace
from src.bot.runtime_coordinator import RuntimeStatus, render_runtime_status_html
from src.auth.supabase_entitlement import SUPABASE_ENTITLEMENT
from src.auth.telegram_group_pricing import TelegramGroupPricing, TELEGRAM_MEMBER_STATUSES
from src.utils.telegram_chat_ids import get_telegram_chat_ids_from_env
_BASIC_COMMANDS = {"start", "help", "id", "top", "diag", "bind", "unbind"}
_BASIC_COMMANDS = {"start", "help", "id", "top", "diag", "bind", "unbind", "markets"}
@@ -21,11 +25,13 @@ class BasicCommandHandler:
io_layer: BotIOLayer,
runtime_status_provider: Callable[[], RuntimeStatus],
config: dict | None = None,
entitlement_service: Any | None = None,
):
self.bot = bot
self.io_layer = io_layer
self.runtime_status_provider = runtime_status_provider
self.config = config or {}
self.entitlement_service = entitlement_service or SUPABASE_ENTITLEMENT
def register(self) -> None:
@self.bot.message_handler(commands=["start", "help"])
@@ -56,6 +62,11 @@ class BasicCommandHandler:
def _markets(message):
self._dispatch(message)
if hasattr(self.bot, "chat_join_request_handler"):
@self.bot.chat_join_request_handler(func=lambda request: True)
def _chat_join_request(request):
self.handle_chat_join_request(request)
@self.bot.message_handler(
content_types=["text"],
func=lambda message: extract_command_name(
@@ -246,6 +257,93 @@ class BasicCommandHandler:
finally:
trace.emit()
def handle_chat_join_request(self, request: Any) -> str:
chat = getattr(request, "chat", None)
user = getattr(request, "from_user", None)
chat_id = getattr(chat, "id", None)
user_id = getattr(user, "id", None)
if chat_id is None or user_id is None:
logger.warning("telegram join request missing chat_id/user_id")
return "ignored:invalid_request"
configured_chat_ids = {str(value).strip() for value in get_telegram_chat_ids_from_env() if str(value).strip()}
configured_chat_ids.update(
str(value).strip()
for value in [
os.getenv("POLYWEATHER_TELEGRAM_GROUP_ID"),
os.getenv("POLYWEATHER_TELEGRAM_TOPICS_GROUP_ID"),
]
if str(value or "").strip()
)
if configured_chat_ids and str(chat_id) not in configured_chat_ids:
logger.info(
"telegram join request ignored for non-configured chat chat_id={} user_id={}",
chat_id,
user_id,
)
return "ignored:chat_not_configured"
try:
supabase_user_ids = self.io_layer.db.list_supabase_user_ids_for_telegram(int(user_id))
except Exception as exc:
logger.warning("telegram join request binding lookup failed user_id={}: {}", user_id, exc)
return "pending:lookup_error"
if not supabase_user_ids:
return self._handle_ineligible_join_request(
chat_id=int(chat_id),
user_id=int(user_id),
reason="unbound",
)
for supabase_user_id in supabase_user_ids:
try:
if self.entitlement_service.has_active_subscription(
supabase_user_id,
respect_requirement=False,
):
self.bot.approve_chat_join_request(int(chat_id), int(user_id))
logger.info(
"telegram join request approved chat_id={} user_id={} supabase_user_id={}",
chat_id,
user_id,
supabase_user_id,
)
return "approved"
except Exception as exc:
logger.warning(
"telegram join request entitlement lookup failed user_id={} supabase_user_id={}: {}",
user_id,
supabase_user_id,
exc,
)
return "pending:entitlement_error"
return self._handle_ineligible_join_request(
chat_id=int(chat_id),
user_id=int(user_id),
reason="no_active_subscription",
)
def _handle_ineligible_join_request(self, chat_id: int, user_id: int, reason: str) -> str:
action = str(os.getenv("POLYWEATHER_TELEGRAM_JOIN_INELIGIBLE_ACTION") or "pending").strip().lower()
if action in {"decline", "reject", "deny"}:
self.bot.decline_chat_join_request(chat_id, user_id)
logger.info(
"telegram join request declined chat_id={} user_id={} reason={}",
chat_id,
user_id,
reason,
)
return f"declined:{reason}"
logger.info(
"telegram join request left pending chat_id={} user_id={} reason={}",
chat_id,
user_id,
reason,
)
return f"pending:{reason}"
def handle_unbind(self, message: Any) -> None:
trace = CommandTrace("/unbind", message)
try:
+1 -1
View File
@@ -103,4 +103,4 @@ def start_bot() -> None:
started_count,
len(runtime_status.loops),
)
bot.infinity_polling()
bot.infinity_polling(allowed_updates=["message", "chat_join_request"])
+32
View File
@@ -988,6 +988,38 @@ class DBManager:
return dict(row)
return None
def list_supabase_user_ids_for_telegram(self, telegram_id: int) -> List[str]:
"""Return all Supabase accounts currently bound to a Telegram user."""
with self._get_connection() as conn:
conn.row_factory = sqlite3.Row
rows = conn.execute(
"""
SELECT supabase_user_id
FROM supabase_bindings
WHERE telegram_id = ?
ORDER BY updated_at DESC, supabase_user_id ASC
""",
(int(telegram_id),),
).fetchall()
ids = {
str(row["supabase_user_id"] or "").strip().lower()
for row in rows
if str(row["supabase_user_id"] or "").strip()
}
legacy = conn.execute(
"""
SELECT supabase_user_id
FROM users
WHERE telegram_id = ?
LIMIT 1
""",
(int(telegram_id),),
).fetchone()
legacy_id = str((legacy["supabase_user_id"] if legacy else "") or "").strip().lower()
if legacy_id:
ids.add(legacy_id)
return sorted(ids)
def search_users(self, query: str, limit: int = 20) -> List[Dict[str, Any]]:
text = str(query or "").strip()
safe_limit = max(1, min(int(limit or 20), 100))