mirror of
https://github.com/pyMC-dev/pyMC_Repeater.git
synced 2026-06-28 05:52:02 +02:00
191 lines
6.5 KiB
Python
191 lines
6.5 KiB
Python
"""Resolve companion config rows by registration name, identity key, or public key prefix."""
|
|
|
|
from __future__ import annotations
|
|
|
|
import logging
|
|
from typing import Any, List, Optional, Set, Tuple
|
|
|
|
from repeater.companion.utils import normalize_companion_identity_key
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
# Minimum hex chars for identity_key / public_key prefix disambiguation (4 bytes)
|
|
_MIN_PREFIX_HEX_LEN = 8
|
|
|
|
|
|
def _companion_registration_name(entry: dict) -> str:
|
|
n = entry.get("name")
|
|
if n is None:
|
|
return ""
|
|
return str(n).strip()
|
|
|
|
|
|
def identity_key_bytes_from_config(identity_key: Any) -> Optional[bytes]:
|
|
"""Parse companion identity_key from YAML (str hex or raw bytes)."""
|
|
if identity_key is None:
|
|
return None
|
|
if isinstance(identity_key, (bytes, bytearray, memoryview)):
|
|
raw = bytes(identity_key)
|
|
return raw if len(raw) in (32, 64) else None
|
|
if isinstance(identity_key, str):
|
|
try:
|
|
raw = bytes.fromhex(normalize_companion_identity_key(identity_key))
|
|
except ValueError:
|
|
return None
|
|
return raw if len(raw) in (32, 64) else None
|
|
return None
|
|
|
|
|
|
def identity_key_hex_normalized(identity_key: Any) -> Optional[str]:
|
|
"""Lowercase hex string of the raw key bytes (64 or 128 chars), or None."""
|
|
raw = identity_key_bytes_from_config(identity_key)
|
|
if raw is None:
|
|
return None
|
|
return raw.hex().lower()
|
|
|
|
|
|
def derive_companion_public_key_hex(identity_key: Any) -> Optional[str]:
|
|
"""Return ed25519 public key hex for a companion seed, or None if invalid."""
|
|
raw = identity_key_bytes_from_config(identity_key)
|
|
if raw is None:
|
|
return None
|
|
try:
|
|
from openhop_core import LocalIdentity
|
|
|
|
identity = LocalIdentity(seed=raw)
|
|
return identity.get_public_key().hex()
|
|
except Exception as e:
|
|
logger.debug("derive_companion_public_key_hex failed: %s", e)
|
|
return None
|
|
|
|
|
|
def suggest_companion_name_from_pubkey(pubkey_hex: str, prefix_len: int = 8) -> str:
|
|
"""Stable default registration name: companion_<first prefix_len hex chars of pubkey>."""
|
|
p = pubkey_hex.strip().lower()
|
|
if p.startswith("0x"):
|
|
p = p[2:]
|
|
if len(p) < prefix_len:
|
|
prefix = p
|
|
else:
|
|
prefix = p[:prefix_len]
|
|
return f"companion_{prefix}"
|
|
|
|
|
|
def unique_suggested_name(
|
|
pubkey_hex: str,
|
|
existing_names: set,
|
|
prefix_len: int = 8,
|
|
) -> str:
|
|
"""Like suggest_companion_name_from_pubkey but appends -2, -3, ... if name collides."""
|
|
base = suggest_companion_name_from_pubkey(pubkey_hex, prefix_len=prefix_len)
|
|
if base not in existing_names:
|
|
return base
|
|
n = 2
|
|
while f"{base}-{n}" in existing_names:
|
|
n += 1
|
|
return f"{base}-{n}"
|
|
|
|
|
|
def find_companion_index(
|
|
companions: List[dict],
|
|
*,
|
|
name: Optional[str] = None,
|
|
identity_key: Optional[str] = None,
|
|
public_key_prefix: Optional[str] = None,
|
|
) -> Tuple[Optional[int], Optional[str]]:
|
|
"""
|
|
Find a single companion list index.
|
|
|
|
Lookup priority when multiple fields are set:
|
|
1) name (non-empty after strip)
|
|
2) identity_key (full hex or unique prefix)
|
|
3) public_key_prefix (unique prefix of derived public key hex)
|
|
|
|
Returns (index, None) on success, or (None, error_message) on failure.
|
|
"""
|
|
name_s = str(name).strip() if name is not None else ""
|
|
idk = str(identity_key).strip() if identity_key is not None else ""
|
|
pkp = str(public_key_prefix).strip() if public_key_prefix is not None else ""
|
|
if pkp.lower().startswith("0x"):
|
|
pkp = pkp[2:].strip()
|
|
pkp = pkp.lower()
|
|
|
|
if idk:
|
|
idk = normalize_companion_identity_key(idk).lower()
|
|
|
|
if name_s:
|
|
matches = [i for i, c in enumerate(companions) if _companion_registration_name(c) == name_s]
|
|
if len(matches) == 1:
|
|
return matches[0], None
|
|
if len(matches) == 0:
|
|
return None, f"Companion '{name_s}' not found"
|
|
return None, f"Multiple companions named '{name_s}'"
|
|
|
|
if idk:
|
|
if len(idk) < _MIN_PREFIX_HEX_LEN:
|
|
return None, (
|
|
f"identity_key lookup must be at least {_MIN_PREFIX_HEX_LEN} hex characters"
|
|
)
|
|
exact: List[int] = []
|
|
prefix_matches: List[int] = []
|
|
for i, c in enumerate(companions):
|
|
h = identity_key_hex_normalized(c.get("identity_key"))
|
|
if not h:
|
|
continue
|
|
if h == idk:
|
|
exact.append(i)
|
|
elif h.startswith(idk):
|
|
prefix_matches.append(i)
|
|
if len(exact) == 1:
|
|
return exact[0], None
|
|
if len(exact) > 1:
|
|
return None, "Multiple companions match identity_key (ambiguous)"
|
|
if len(prefix_matches) == 1:
|
|
return prefix_matches[0], None
|
|
if len(prefix_matches) == 0:
|
|
return None, "No companion matches identity_key"
|
|
return None, "Multiple companions match identity_key prefix (ambiguous)"
|
|
|
|
if pkp:
|
|
if len(pkp) < _MIN_PREFIX_HEX_LEN:
|
|
return None, (
|
|
f"public_key_prefix must be at least {_MIN_PREFIX_HEX_LEN} hex characters"
|
|
)
|
|
matches: List[int] = []
|
|
for i, c in enumerate(companions):
|
|
pub = derive_companion_public_key_hex(c.get("identity_key"))
|
|
if pub and pub.lower().startswith(pkp):
|
|
matches.append(i)
|
|
if len(matches) == 1:
|
|
return matches[0], None
|
|
if len(matches) == 0:
|
|
return None, "No companion matches public_key_prefix"
|
|
return None, "Multiple companions match public_key_prefix (ambiguous)"
|
|
|
|
return None, "Missing companion lookup: provide name, identity_key, or public_key_prefix"
|
|
|
|
|
|
def heal_companion_empty_names(companions: List[dict]) -> bool:
|
|
"""
|
|
Assign companion_<pubkeyPrefix> names to entries with missing/blank registration names.
|
|
Mutates companions in place. Returns True if any entry was updated.
|
|
"""
|
|
names_in_use: Set[str] = set()
|
|
for c in companions:
|
|
n = _companion_registration_name(c)
|
|
if n:
|
|
names_in_use.add(n)
|
|
changed = False
|
|
for entry in companions:
|
|
if _companion_registration_name(entry):
|
|
continue
|
|
pk = derive_companion_public_key_hex(entry.get("identity_key"))
|
|
if not pk:
|
|
logger.warning("Skipping companion name heal: invalid or missing identity_key")
|
|
continue
|
|
new_name = unique_suggested_name(pk, names_in_use)
|
|
entry["name"] = new_name
|
|
names_in_use.add(new_name)
|
|
changed = True
|
|
return changed
|