= 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) ?>