= 5) { $conv[] = ($buffer >> ($bits - 5)) & 31; $bits -= 5; } } if ($bits > 0) { $conv[] = ($buffer << (5 - $bits)) & 31; } // Compute checksum — HRP values are positions of each char in the charset. $values = array_merge( array_map(function($c) { return strpos('qpzry9x8gf2tvdw0s3jn54khce6mua7l', $c); }, str_split('npub')), $conv ); $gen = [0x3b6a57b2, 0x26508e6d, 0x1ea119fa, 0x3d4233dd, 0x2a1462b3]; $chk = 1; foreach ($values as $v) { $top = $chk >> 25; $chk = (($chk & 0x1ffffff) << 5) ^ $v; for ($i = 0; $i < 5; $i++) { if (($top >> $i) & 1) $chk ^= $gen[$i]; } } $chk ^= 1; $checksum = []; for ($i = 0; $i < 6; $i++) { $checksum[] = ($chk >> (5 * (5 - $i))) & 31; } $result = 'npub1'; foreach (array_merge($conv, $checksum) as $v) { $result .= $charset[$v]; } return $result; } /** Truncate a string to $len chars with ellipsis. */ function trunc(string $s, int $len = 16): string { if (strlen($s) <= $len) return $s; return substr($s, 0, $len) . '…'; } /** Build pagination links HTML. */ function pagination(int $page, int $per_page, int $total, string $base_url): string { $total_pages = max(1, ceil($total / $per_page)); if ($total_pages <= 1) return ''; $html = ''; return $html; } /** Get the current page number from the query string. */ function current_page(): int { $p = intval($_GET['page'] ?? 1); return max(1, $p); } /** JSON response helper for AJAX endpoints. */ function json_response($data): void { header('Content-Type: application/json'); echo json_encode($data, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE); exit; } /** Get a query parameter with a default. */ function query_param(string $key, $default = null) { return $_GET[$key] ?? $default; } /** * Batch-resolve profiles from the profiles cache table. * Returns [pubkey_hex => ['name'=>..., 'display_name'=>..., * 'best_name'=>..., 'picture'=>..., 'nip05'=>...]]. * Pubkeys with no cached profile are absent from the result. */ function profile_map(array $pubkeys): array { if (empty($pubkeys)) return []; $pdo = db(); $map = []; try { // Build parameterized IN clause: WHERE pubkey = ANY(?) // PDO PostgreSQL supports passing an array as a string literal. $placeholders = implode(',', array_fill(0, count($pubkeys), '?')); $stmt = $pdo->prepare( "SELECT pubkey, name, display_name, picture, nip05 FROM profiles " . "WHERE pubkey IN ($placeholders)" ); $stmt->execute(array_values($pubkeys)); $rows = $stmt->fetchAll(); foreach ($rows as $r) { $map[$r['pubkey']] = [ 'name' => $r['name'] ?? '', 'display_name' => $r['display_name'] ?? '', 'best_name' => profile_display_name($r), 'picture' => $r['picture'] ?? '', 'nip05' => $r['nip05'] ?? '', ]; } } catch (PDOException $e) {} return $map; } /** * Apply the profile_name_preference config key to resolve the display name. * Never returns null. Returns '' if neither field is set. */ function profile_display_name(array $profile): string { $name = $profile['name'] ?? ''; $display_name = $profile['display_name'] ?? ''; static $pref = null; if ($pref === null) { try { $pdo = db(); $pref = $pdo->query("SELECT value FROM config WHERE key = 'profile_name_preference'")->fetchColumn() ?: 'display_name'; } catch (PDOException $e) { $pref = 'display_name'; } } if ($pref === 'name') { return $name !== '' ? $name : $display_name; } // Default: prefer display_name, fall back to name. return $display_name !== '' ? $display_name : $name; } /** * Render the shared HTML header with side navigation. * Pass the active page name to highlight the current nav item. */ function admin_header(string $active = '', string $title = 'C-Relay-PG Admin'): void { $pages = [ 'dashboard' => ['index.php', 'Dashboard'], 'follows' => ['follows.php', 'Follows'], 'relays' => ['relays.php', 'Relays'], 'config' => ['config-edit.php', 'Config'], 'inbox' => ['inbox.php', 'Inbox'], ]; ?> <?= e($title) ?>
RELAY