One pass over the LOW-tier review items plus two folded issues: - Delete validate_journal_entry (dead since the Fava migration; it validated the pre-string-amount model) with its exports, unused crud imports, and tests. Beancount validates entries now. - Migration m006: UNIQUE index on user_roles(user_id, role_id) after deduping; assign_user_role inserts with ON CONFLICT DO NOTHING and returns the existing assignment — closes the auto-assign check-then-act race on concurrent logins. - Extract _get_username_from_user_id (110 lines in views_api, fresh LNbits Database per call inside per-row hot paths) into user_lookup.py with one shared core-DB handle, a 60s TTL cache and a batch get_usernames API (review #18). - Receivable-entry responses report CLEARED, matching the flag the formatter actually writes; PENDING misled the UI (libra-#35). - Replace the remaining print() calls in tasks.py with logger. - get_all_accounts derives valid roots from account_utils.ACCOUNT_TYPE_ROOTS instead of a hardcoded tuple, and the no-op per-test rate-limit reset is gone (libra-#54). - Delete migrations_old.py.bak, MIGRATION_SQUASH_SUMMARY.md, docs/PHASE*_COMPLETE.md and the rendered .html; .gitignore data/ (it holds the runtime .lnbits_auth_key secret). - Track docs/CODE-REVIEW-2026-06.md with finding statuses updated for the PR #55-#59 + chore/hygiene series. - CLAUDE.md notes LNbits pins Pydantic v1: keep .dict(), don't "modernize" to .model_dump(). Note: format_payment_entry's is_payable docstring (flagged in review follow-up) turned out to be consistent with the body — no change. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
126 lines
4.4 KiB
Python
126 lines
4.4 KiB
Python
"""Username resolution for UI display.
|
|
|
|
Extracted from views_api (CODE-REVIEW-2026-06 #18): the old helper
|
|
constructed a fresh LNbits `Database` per call inside per-row hot paths
|
|
(entry listings, all-user balances). This module keeps one shared core-DB
|
|
handle and a short TTL cache, so listing N rows for the same few users
|
|
costs one lookup per unique user per TTL window instead of one per row.
|
|
|
|
Accepted id shapes (they all occur in ledger data):
|
|
- Full UUID with dashes (36 chars): "375ec158-686c-4a21-b44d-a51cc90ef07d"
|
|
- Dashless UUID (32 chars): "375ec158686c4a21b44da51cc90ef07d"
|
|
- Partial id from account names (8 chars): "375ec158"
|
|
"""
|
|
|
|
from typing import Dict, Iterable, Optional
|
|
|
|
from lnbits.core.crud.users import get_user
|
|
from lnbits.db import Database
|
|
from lnbits.utils.cache import Cache
|
|
from loguru import logger
|
|
|
|
# One shared handle to the LNbits core DB (username lives on core
|
|
# `accounts`, not in libra's extension DB).
|
|
_core_db = Database("database")
|
|
|
|
_username_cache = Cache()
|
|
_USERNAME_CACHE_TTL = 60 # seconds — usernames change rarely
|
|
|
|
|
|
def _dashed(user_id: str) -> str:
|
|
return (
|
|
f"{user_id[0:8]}-{user_id[8:12]}-{user_id[12:16]}"
|
|
f"-{user_id[16:20]}-{user_id[20:32]}"
|
|
)
|
|
|
|
|
|
async def _resolve(user_id: str) -> str:
|
|
from .crud import get_all_user_wallet_settings
|
|
|
|
# Case 1: full UUID with dashes
|
|
if len(user_id) == 36 and user_id.count('-') == 4:
|
|
user = await get_user(user_id)
|
|
return user.username if user and user.username else f"User-{user_id[:8]}"
|
|
|
|
# Case 2: dashless 32-char UUID — libra user settings first, then
|
|
# the LNbits core DB directly
|
|
if len(user_id) == 32 and '-' not in user_id:
|
|
try:
|
|
user_id_with_dashes = _dashed(user_id)
|
|
|
|
user_settings = await get_all_user_wallet_settings()
|
|
for setting in user_settings:
|
|
if setting.id == user_id_with_dashes:
|
|
user = await get_user(setting.id)
|
|
return (
|
|
user.username
|
|
if user and user.username
|
|
else f"User-{user_id[:8]}"
|
|
)
|
|
|
|
async with _core_db.connect() as conn:
|
|
row = await conn.fetchone(
|
|
"SELECT id, username FROM accounts WHERE id = :user_id LIMIT 1",
|
|
{"user_id": user_id_with_dashes},
|
|
)
|
|
if row and row["username"]:
|
|
return row["username"]
|
|
|
|
return f"User-{user_id[:8]}"
|
|
except Exception as e:
|
|
logger.error(f"Error looking up user by dashless UUID {user_id}: {e}")
|
|
return f"User-{user_id[:8]}"
|
|
|
|
# Case 3: 8-char partial id from an account name — resolve to a full
|
|
# id via libra user settings
|
|
if len(user_id) == 8:
|
|
try:
|
|
user_settings = await get_all_user_wallet_settings()
|
|
for setting in user_settings:
|
|
if setting.id.startswith(user_id):
|
|
user = await get_user(setting.id)
|
|
return (
|
|
user.username
|
|
if user and user.username
|
|
else f"User-{user_id}"
|
|
)
|
|
return f"User-{user_id}"
|
|
except Exception as e:
|
|
logger.error(f"Error looking up user by partial ID {user_id}: {e}")
|
|
return f"User-{user_id}"
|
|
|
|
# Case 4: unknown shape — try as-is, fall back
|
|
try:
|
|
user = await get_user(user_id)
|
|
return user.username if user and user.username else f"User-{user_id[:8]}"
|
|
except Exception:
|
|
return f"User-{user_id[:8]}"
|
|
|
|
|
|
async def get_username(user_id: str) -> Optional[str]:
|
|
"""Resolve a user id (any accepted shape) to a display username.
|
|
|
|
Returns a "User-{short}" fallback when no username exists, or None
|
|
for falsy input.
|
|
"""
|
|
if not user_id:
|
|
return None
|
|
|
|
cache_key = f"username:{user_id}"
|
|
cached = _username_cache.get(cache_key)
|
|
if cached is not None:
|
|
return cached
|
|
|
|
result = await _resolve(user_id)
|
|
_username_cache.set(cache_key, result, _USERNAME_CACHE_TTL)
|
|
return result
|
|
|
|
|
|
async def get_usernames(user_ids: Iterable[str]) -> Dict[str, str]:
|
|
"""Resolve many user ids at once, deduplicated and cache-backed."""
|
|
result: Dict[str, str] = {}
|
|
for user_id in {u for u in user_ids if u}:
|
|
username = await get_username(user_id)
|
|
if username is not None:
|
|
result[user_id] = username
|
|
return result
|