Sync Telegram profile fields to Supabase and show user IDs

This commit is contained in:
2569718930@qq.com
2026-04-01 02:13:17 +08:00
parent a8462e188b
commit 3bbd8774a4
3 changed files with 234 additions and 4 deletions
+4 -1
View File
@@ -810,6 +810,7 @@ export function OpsDashboard() {
<div className="font-semibold text-slate-100">{item.email || "-"}</div>
<div className="mt-1 text-xs text-slate-500">{item.username || "-"}</div>
<div className="mt-3 grid gap-2">
<MobileField label="User ID" value={item.user_id || "-"} mono />
<MobileField label="注册时间" value={formatDateTime(item.registered_at)} />
<MobileField label="到期时间" value={formatDateTime(item.expires_at)} />
</div>
@@ -827,6 +828,7 @@ export function OpsDashboard() {
<tr>
<th className="px-4 py-3"></th>
<th className="px-4 py-3"></th>
<th className="px-4 py-3">User ID</th>
<th className="px-4 py-3"></th>
<th className="px-4 py-3"></th>
</tr>
@@ -836,13 +838,14 @@ export function OpsDashboard() {
<tr key={`${item.user_id}-${item.expires_at || ""}`}>
<td className="px-4 py-3">{item.email || "-"}</td>
<td className="px-4 py-3">{item.username || "-"}</td>
<td className="px-4 py-3 font-mono text-xs text-slate-300">{item.user_id || "-"}</td>
<td className="px-4 py-3">{formatDateTime(item.registered_at)}</td>
<td className="px-4 py-3">{formatDateTime(item.expires_at)}</td>
</tr>
))}
{!memberships.length ? (
<tr>
<td className="px-4 py-4 text-slate-500" colSpan={4}>
<td className="px-4 py-4 text-slate-500" colSpan={5}>
</td>
</tr>
@@ -0,0 +1,94 @@
from __future__ import annotations
import argparse
import os
import sqlite3
import sys
from typing import Any
ROOT_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
if ROOT_DIR not in sys.path:
sys.path.insert(0, ROOT_DIR)
from src.database.db_manager import DBManager
def main() -> None:
parser = argparse.ArgumentParser(
description="Backfill Supabase public.profiles.telegram_* fields from local SQLite bindings.",
)
parser.add_argument(
"--dry-run",
action="store_true",
help="Print rows that would be synced without writing to Supabase.",
)
args = parser.parse_args()
db = DBManager()
synced = 0
skipped = 0
rows_out: list[dict[str, Any]] = []
with db._get_connection() as conn: # noqa: SLF001
conn.row_factory = sqlite3.Row
rows = conn.execute(
"""
SELECT
lower(trim(COALESCE(b.supabase_user_id, ''))) AS supabase_user_id,
b.telegram_id AS telegram_id,
COALESCE(u.username, '') AS telegram_username
FROM supabase_bindings b
LEFT JOIN users u
ON u.telegram_id = b.telegram_id
WHERE trim(COALESCE(b.supabase_user_id, '')) <> ''
ORDER BY b.updated_at DESC, b.supabase_user_id ASC
"""
).fetchall()
for row in rows:
supabase_user_id = str(row["supabase_user_id"] or "").strip().lower()
if not supabase_user_id:
skipped += 1
continue
telegram_id = int(row["telegram_id"] or 0)
telegram_username = str(row["telegram_username"] or "").strip()
payload = {
"supabase_user_id": supabase_user_id,
"telegram_id": telegram_id,
"telegram_username": telegram_username or None,
}
rows_out.append(payload)
if args.dry_run:
continue
ok = db._sync_supabase_profile_telegram_fields( # noqa: SLF001
supabase_user_id=supabase_user_id,
telegram_id=telegram_id,
telegram_username=telegram_username,
)
if ok:
synced += 1
else:
skipped += 1
if args.dry_run:
print(
{
"mode": "dry_run",
"rows": len(rows_out),
"items": rows_out,
}
)
return
print(
{
"mode": "sync",
"synced": synced,
"skipped": skipped,
"rows": len(rows_out),
}
)
if __name__ == "__main__":
main()
+136 -3
View File
@@ -4,6 +4,8 @@ import hashlib
import json
from datetime import datetime, timedelta
from typing import Optional, Dict, Any, List
import requests
from loguru import logger
@@ -22,6 +24,93 @@ class DBManager:
def _get_connection(self):
return sqlite3.connect(self.db_path)
def _supabase_profiles_endpoint(self) -> str:
supabase_url = str(os.getenv("SUPABASE_URL") or "").strip().rstrip("/")
if not supabase_url:
return ""
return f"{supabase_url}/rest/v1/profiles"
def _supabase_service_headers(self) -> Dict[str, str]:
service_role_key = str(os.getenv("SUPABASE_SERVICE_ROLE_KEY") or "").strip()
if not service_role_key:
return {}
return {
"apikey": service_role_key,
"Authorization": f"Bearer {service_role_key}",
"Accept": "application/json",
"Content-Type": "application/json",
"Prefer": "return=minimal",
}
def _sync_supabase_profile_telegram_fields(
self,
*,
supabase_user_id: str,
telegram_id: Optional[int],
telegram_username: Optional[str],
) -> bool:
normalized_uid = str(supabase_user_id or "").strip().lower()
endpoint = self._supabase_profiles_endpoint()
headers = self._supabase_service_headers()
if not normalized_uid or not endpoint or not headers:
return False
payload = {
"telegram_user_id": int(telegram_id) if telegram_id is not None else None,
"telegram_username": str(telegram_username or "").strip() or None,
"updated_at": datetime.now().isoformat(),
}
try:
response = requests.patch(
endpoint,
params={"id": f"eq.{normalized_uid}"},
json=payload,
headers=headers,
timeout=8,
)
if response.status_code not in {200, 204}:
logger.warning(
"supabase profiles telegram sync failed user_id={} status={} body={}",
normalized_uid,
response.status_code,
(response.text or "")[:240],
)
return False
return True
except Exception as exc:
logger.warning(
"supabase profiles telegram sync error user_id={}: {}",
normalized_uid,
exc,
)
return False
def _sync_bound_supabase_profiles_for_telegram(
self,
*,
telegram_id: int,
telegram_username: Optional[str],
) -> None:
with self._get_connection() as conn:
conn.row_factory = sqlite3.Row
rows = conn.execute(
"""
SELECT supabase_user_id
FROM supabase_bindings
WHERE telegram_id = ?
""",
(int(telegram_id),),
).fetchall()
for row in rows:
user_id = str((row["supabase_user_id"] if row else "") or "").strip().lower()
if not user_id:
continue
self._sync_supabase_profile_telegram_fields(
supabase_user_id=user_id,
telegram_id=telegram_id,
telegram_username=telegram_username,
)
def _init_db(self):
"""Create tables if they don't exist."""
# Ensure directory exists
@@ -746,6 +835,10 @@ class DBManager:
username = excluded.username
""", (telegram_id, username))
conn.commit()
self._sync_bound_supabase_profiles_for_telegram(
telegram_id=int(telegram_id),
telegram_username=username,
)
def bind_supabase_identity(
self,
@@ -831,7 +924,21 @@ class DBManager:
""",
(normalized_email, telegram_id),
)
user_row = conn.execute(
"""
SELECT username
FROM users
WHERE telegram_id = ?
LIMIT 1
""",
(telegram_id,),
).fetchone()
conn.commit()
self._sync_supabase_profile_telegram_fields(
supabase_user_id=normalized_uid,
telegram_id=int(telegram_id),
telegram_username=str((user_row["username"] if user_row else "") or "").strip(),
)
return {"ok": True, "reason": "already_bound_same"}
conn.execute(
@@ -842,7 +949,21 @@ class DBManager:
""",
(normalized_uid, normalized_email, telegram_id),
)
user_row = conn.execute(
"""
SELECT username
FROM users
WHERE telegram_id = ?
LIMIT 1
""",
(telegram_id,),
).fetchone()
conn.commit()
self._sync_supabase_profile_telegram_fields(
supabase_user_id=normalized_uid,
telegram_id=int(telegram_id),
telegram_username=str((user_row["username"] if user_row else "") or "").strip(),
)
return {"ok": True, "reason": "bound"}
def unbind_supabase_identity(self, telegram_id: int) -> Dict[str, Any]:
@@ -863,11 +984,17 @@ class DBManager:
SELECT supabase_user_id
FROM supabase_bindings
WHERE telegram_id = ?
LIMIT 1
""",
(int(telegram_id),),
).fetchone()
if not current_uid and not links:
).fetchall()
linked_user_ids = {
str((row["supabase_user_id"] if row else "") or "").strip().lower()
for row in links
}
if current_uid:
linked_user_ids.add(current_uid.lower())
linked_user_ids = {item for item in linked_user_ids if item}
if not current_uid and not linked_user_ids:
return {"ok": True, "reason": "not_bound"}
conn.execute(
@@ -886,6 +1013,12 @@ class DBManager:
(telegram_id,),
)
conn.commit()
for user_id in linked_user_ids:
self._sync_supabase_profile_telegram_fields(
supabase_user_id=user_id,
telegram_id=None,
telegram_username=None,
)
return {"ok": True, "reason": "unbound", "previous_supabase_user_id": current_uid}
def add_message_activity(