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="font-semibold text-slate-100">{item.email || "-"}</div>
<div className="mt-1 text-xs text-slate-500">{item.username || "-"}</div> <div className="mt-1 text-xs text-slate-500">{item.username || "-"}</div>
<div className="mt-3 grid gap-2"> <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.registered_at)} />
<MobileField label="到期时间" value={formatDateTime(item.expires_at)} /> <MobileField label="到期时间" value={formatDateTime(item.expires_at)} />
</div> </div>
@@ -827,6 +828,7 @@ export function OpsDashboard() {
<tr> <tr>
<th className="px-4 py-3"></th> <th className="px-4 py-3"></th>
<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>
<th className="px-4 py-3"></th> <th className="px-4 py-3"></th>
</tr> </tr>
@@ -836,13 +838,14 @@ export function OpsDashboard() {
<tr key={`${item.user_id}-${item.expires_at || ""}`}> <tr key={`${item.user_id}-${item.expires_at || ""}`}>
<td className="px-4 py-3">{item.email || "-"}</td> <td className="px-4 py-3">{item.email || "-"}</td>
<td className="px-4 py-3">{item.username || "-"}</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.registered_at)}</td>
<td className="px-4 py-3">{formatDateTime(item.expires_at)}</td> <td className="px-4 py-3">{formatDateTime(item.expires_at)}</td>
</tr> </tr>
))} ))}
{!memberships.length ? ( {!memberships.length ? (
<tr> <tr>
<td className="px-4 py-4 text-slate-500" colSpan={4}> <td className="px-4 py-4 text-slate-500" colSpan={5}>
</td> </td>
</tr> </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 import json
from datetime import datetime, timedelta from datetime import datetime, timedelta
from typing import Optional, Dict, Any, List from typing import Optional, Dict, Any, List
import requests
from loguru import logger from loguru import logger
@@ -22,6 +24,93 @@ class DBManager:
def _get_connection(self): def _get_connection(self):
return sqlite3.connect(self.db_path) 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): def _init_db(self):
"""Create tables if they don't exist.""" """Create tables if they don't exist."""
# Ensure directory exists # Ensure directory exists
@@ -746,6 +835,10 @@ class DBManager:
username = excluded.username username = excluded.username
""", (telegram_id, username)) """, (telegram_id, username))
conn.commit() conn.commit()
self._sync_bound_supabase_profiles_for_telegram(
telegram_id=int(telegram_id),
telegram_username=username,
)
def bind_supabase_identity( def bind_supabase_identity(
self, self,
@@ -831,7 +924,21 @@ class DBManager:
""", """,
(normalized_email, telegram_id), (normalized_email, telegram_id),
) )
user_row = conn.execute(
"""
SELECT username
FROM users
WHERE telegram_id = ?
LIMIT 1
""",
(telegram_id,),
).fetchone()
conn.commit() 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"} return {"ok": True, "reason": "already_bound_same"}
conn.execute( conn.execute(
@@ -842,7 +949,21 @@ class DBManager:
""", """,
(normalized_uid, normalized_email, telegram_id), (normalized_uid, normalized_email, telegram_id),
) )
user_row = conn.execute(
"""
SELECT username
FROM users
WHERE telegram_id = ?
LIMIT 1
""",
(telegram_id,),
).fetchone()
conn.commit() 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"} return {"ok": True, "reason": "bound"}
def unbind_supabase_identity(self, telegram_id: int) -> Dict[str, Any]: def unbind_supabase_identity(self, telegram_id: int) -> Dict[str, Any]:
@@ -863,11 +984,17 @@ class DBManager:
SELECT supabase_user_id SELECT supabase_user_id
FROM supabase_bindings FROM supabase_bindings
WHERE telegram_id = ? WHERE telegram_id = ?
LIMIT 1
""", """,
(int(telegram_id),), (int(telegram_id),),
).fetchone() ).fetchall()
if not current_uid and not links: 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"} return {"ok": True, "reason": "not_bound"}
conn.execute( conn.execute(
@@ -886,6 +1013,12 @@ class DBManager:
(telegram_id,), (telegram_id,),
) )
conn.commit() 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} return {"ok": True, "reason": "unbound", "previous_supabase_user_id": current_uid}
def add_message_activity( def add_message_activity(